mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
mcp_ticketer/cli/main.py CHANGED
@@ -5,28 +5,35 @@ import json
5
5
  import os
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Optional
8
+ from typing import Any
9
9
 
10
10
  import typer
11
11
  from dotenv import load_dotenv
12
12
  from rich.console import Console
13
- from rich.table import Table
14
-
15
- from ..__version__ import __version__
16
- from ..core import AdapterRegistry, Priority, TicketState
17
- from ..core.models import SearchQuery, Comment
18
- from ..queue import Queue, QueueStatus, WorkerManager
19
- from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
20
- from ..queue.ticket_registry import TicketRegistry
21
13
 
22
14
  # Import adapters module to trigger registration
23
15
  import mcp_ticketer.adapters # noqa: F401
16
+
17
+ from ..__version__ import __version__
18
+ from ..core import AdapterRegistry
24
19
  from .configure import configure_wizard, set_adapter_config, show_current_config
25
20
  from .diagnostics import run_diagnostics
26
21
  from .discover import app as discover_app
27
- from .linear_commands import app as linear_app
22
+ from .init_command import init
23
+ from .install_mcp_server import (
24
+ install_mcp_server,
25
+ list_mcp_servers,
26
+ uninstall_mcp_server,
27
+ )
28
+ from .instruction_commands import app as instruction_app
29
+ from .mcp_server_commands import mcp_app
28
30
  from .migrate_config import migrate_config_command
31
+ from .platform_commands import app as platform_app
32
+ from .platform_installer import install, remove, uninstall
33
+ from .project_update_commands import app as project_update_app
29
34
  from .queue_commands import app as queue_app
35
+ from .setup_command import setup
36
+ from .ticket_commands import app as ticket_app
30
37
 
31
38
  # Load environment variables from .env files
32
39
  # Priority: .env.local (highest) > .env (base)
@@ -48,11 +55,11 @@ app = typer.Typer(
48
55
  console = Console()
49
56
 
50
57
 
51
- def version_callback(value: bool):
58
+ def version_callback(value: bool) -> None:
52
59
  """Print version and exit."""
53
60
  if value:
54
61
  console.print(f"mcp-ticketer version {__version__}")
55
- raise typer.Exit()
62
+ raise typer.Exit() from None
56
63
 
57
64
 
58
65
  @app.callback()
@@ -65,7 +72,7 @@ def main_callback(
65
72
  is_eager=True,
66
73
  help="Show version and exit",
67
74
  ),
68
- ):
75
+ ) -> None:
69
76
  """MCP Ticketer - Universal ticket management interface."""
70
77
  pass
71
78
 
@@ -83,7 +90,7 @@ class AdapterType(str, Enum):
83
90
  GITHUB = "github"
84
91
 
85
92
 
86
- def load_config(project_dir: Optional[Path] = None) -> dict:
93
+ def load_config(project_dir: Path | None = None) -> dict:
87
94
  """Load configuration from project-local config file ONLY.
88
95
 
89
96
  SECURITY: This method ONLY reads from the current project directory
@@ -91,6 +98,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
91
98
  from user home directory or system-wide locations.
92
99
 
93
100
  Args:
101
+ ----
94
102
  project_dir: Optional project directory to load config from
95
103
 
96
104
  Resolution order:
@@ -98,6 +106,7 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
98
106
  2. Default to aitrackdown adapter
99
107
 
100
108
  Returns:
109
+ -------
101
110
  Configuration dictionary with adapter and config keys.
102
111
  Defaults to aitrackdown if no local config exists.
103
112
 
@@ -145,13 +154,14 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
145
154
  return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
146
155
 
147
156
 
148
- def _discover_from_env_files() -> Optional[str]:
157
+ def _discover_from_env_files() -> str | None:
149
158
  """Discover adapter configuration from .env or .env.local files.
150
159
 
151
160
  Returns:
161
+ -------
152
162
  Adapter name if discovered, None otherwise
163
+
153
164
  """
154
- import os
155
165
  import logging
156
166
  from pathlib import Path
157
167
 
@@ -166,12 +176,12 @@ def _discover_from_env_files() -> Optional[str]:
166
176
  try:
167
177
  # Simple .env parsing (key=value format)
168
178
  env_vars = {}
169
- with open(env_path, 'r') as f:
179
+ with open(env_path) as f:
170
180
  for line in f:
171
181
  line = line.strip()
172
- if line and not line.startswith('#') and '=' in line:
173
- key, value = line.split('=', 1)
174
- env_vars[key.strip()] = value.strip().strip('"\'')
182
+ if line and not line.startswith("#") and "=" in line:
183
+ key, value = line.split("=", 1)
184
+ env_vars[key.strip()] = value.strip().strip("\"'")
175
185
 
176
186
  # Check for adapter-specific variables
177
187
  if env_vars.get("LINEAR_API_KEY"):
@@ -194,7 +204,9 @@ def _save_adapter_to_config(adapter_name: str) -> None:
194
204
  """Save adapter configuration to config file.
195
205
 
196
206
  Args:
207
+ ----
197
208
  adapter_name: Name of the adapter to save as default
209
+
198
210
  """
199
211
  import logging
200
212
 
@@ -243,9 +255,11 @@ def merge_config(updates: dict) -> dict:
243
255
  """Merge updates into existing config.
244
256
 
245
257
  Args:
258
+ ----
246
259
  updates: Configuration updates to merge
247
260
 
248
261
  Returns:
262
+ -------
249
263
  Updated configuration
250
264
 
251
265
  """
@@ -268,11 +282,12 @@ def merge_config(updates: dict) -> dict:
268
282
 
269
283
 
270
284
  def get_adapter(
271
- override_adapter: Optional[str] = None, override_config: Optional[dict] = None
272
- ):
285
+ override_adapter: str | None = None, override_config: dict | None = None
286
+ ) -> Any:
273
287
  """Get configured adapter instance.
274
288
 
275
289
  Args:
290
+ ----
276
291
  override_adapter: Override the default adapter type
277
292
  override_config: Override configuration for the adapter
278
293
 
@@ -300,7 +315,6 @@ def get_adapter(
300
315
  adapter_config = config["config"]
301
316
 
302
317
  # Add environment variables for authentication
303
- import os
304
318
 
305
319
  if adapter_type == "linear":
306
320
  if not adapter_config.get("api_key"):
@@ -317,393 +331,20 @@ def get_adapter(
317
331
  return AdapterRegistry.get_adapter(adapter_type, adapter_config)
318
332
 
319
333
 
320
- @app.command()
321
- def init(
322
- adapter: Optional[str] = typer.Option(
323
- None,
324
- "--adapter",
325
- "-a",
326
- help="Adapter type to use (auto-detected from .env if not specified)",
327
- ),
328
- project_path: Optional[str] = typer.Option(
329
- None, "--path", help="Project path (default: current directory)"
330
- ),
331
- global_config: bool = typer.Option(
332
- False,
333
- "--global",
334
- "-g",
335
- help="Save to global config instead of project-specific",
336
- ),
337
- base_path: Optional[str] = typer.Option(
338
- None,
339
- "--base-path",
340
- "-p",
341
- help="Base path for ticket storage (AITrackdown only)",
342
- ),
343
- api_key: Optional[str] = typer.Option(
344
- None, "--api-key", help="API key for Linear or API token for JIRA"
345
- ),
346
- team_id: Optional[str] = typer.Option(
347
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
348
- ),
349
- jira_server: Optional[str] = typer.Option(
350
- None,
351
- "--jira-server",
352
- help="JIRA server URL (e.g., https://company.atlassian.net)",
353
- ),
354
- jira_email: Optional[str] = typer.Option(
355
- None, "--jira-email", help="JIRA user email for authentication"
356
- ),
357
- jira_project: Optional[str] = typer.Option(
358
- None, "--jira-project", help="Default JIRA project key"
359
- ),
360
- github_owner: Optional[str] = typer.Option(
361
- None, "--github-owner", help="GitHub repository owner"
362
- ),
363
- github_repo: Optional[str] = typer.Option(
364
- None, "--github-repo", help="GitHub repository name"
365
- ),
366
- github_token: Optional[str] = typer.Option(
367
- None, "--github-token", help="GitHub Personal Access Token"
368
- ),
369
- ) -> None:
370
- """Initialize mcp-ticketer for the current project.
371
-
372
- Creates .mcp-ticketer/config.json in the current directory with
373
- auto-detected or specified adapter configuration.
374
-
375
- Examples:
376
- # Auto-detect from .env.local
377
- mcp-ticketer init
378
-
379
- # Force specific adapter
380
- mcp-ticketer init --adapter linear
381
-
382
- # Initialize for different project
383
- mcp-ticketer init --path /path/to/project
384
-
385
- # Save globally (not recommended)
386
- mcp-ticketer init --global
387
-
388
- """
389
- from pathlib import Path
390
-
391
- from ..core.env_discovery import discover_config
392
- from ..core.project_config import ConfigResolver
393
-
394
- # Determine project path
395
- proj_path = Path(project_path) if project_path else Path.cwd()
396
-
397
- # Check if already initialized (unless using --global)
398
- if not global_config:
399
- config_path = proj_path / ".mcp-ticketer" / "config.json"
400
-
401
- if config_path.exists():
402
- if not typer.confirm(
403
- f"Configuration already exists at {config_path}. Overwrite?",
404
- default=False,
405
- ):
406
- console.print("[yellow]Initialization cancelled.[/yellow]")
407
- raise typer.Exit(0)
408
-
409
- # 1. Try auto-discovery if no adapter specified
410
- discovered = None
411
- adapter_type = adapter
412
-
413
- if not adapter_type:
414
- console.print(
415
- "[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
416
- )
417
- discovered = discover_config(proj_path)
418
-
419
- if discovered and discovered.adapters:
420
- primary = discovered.get_primary_adapter()
421
- if primary:
422
- adapter_type = primary.adapter_type
423
- console.print(
424
- f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
425
- )
426
-
427
- # Show what was discovered
428
- console.print(
429
- f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
430
- )
431
- console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
432
- else:
433
- adapter_type = "aitrackdown" # Fallback
434
- console.print(
435
- "[yellow]⚠ No credentials found, defaulting to aitrackdown[/yellow]"
436
- )
437
- else:
438
- adapter_type = "aitrackdown" # Fallback
439
- console.print(
440
- "[yellow]⚠ No .env files found, defaulting to aitrackdown[/yellow]"
441
- )
442
-
443
- # 2. Create configuration based on adapter type
444
- config = {"default_adapter": adapter_type, "adapters": {}}
445
-
446
- # 3. If discovered and matches adapter_type, use discovered config
447
- if discovered and adapter_type != "aitrackdown":
448
- discovered_adapter = discovered.get_adapter_by_type(adapter_type)
449
- if discovered_adapter:
450
- adapter_config = discovered_adapter.config.copy()
451
- # Ensure the config has the correct 'type' field
452
- adapter_config["type"] = adapter_type
453
- # Remove 'adapter' field if present (legacy)
454
- adapter_config.pop("adapter", None)
455
- config["adapters"][adapter_type] = adapter_config
456
-
457
- # 4. Handle manual configuration for specific adapters
458
- if adapter_type == "aitrackdown":
459
- config["adapters"]["aitrackdown"] = {
460
- "type": "aitrackdown",
461
- "base_path": base_path or ".aitrackdown"
462
- }
463
-
464
- elif adapter_type == "linear":
465
- # If not auto-discovered, build from CLI params
466
- if adapter_type not in config["adapters"]:
467
- linear_config = {}
468
-
469
- # Team ID
470
- if team_id:
471
- linear_config["team_id"] = team_id
472
-
473
- # API Key
474
- linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
475
- if linear_api_key:
476
- linear_config["api_key"] = linear_api_key
477
- elif not discovered:
478
- console.print("[yellow]Warning:[/yellow] No Linear API key provided.")
479
- console.print(
480
- "Set LINEAR_API_KEY environment variable or use --api-key option"
481
- )
482
-
483
- if linear_config:
484
- linear_config["type"] = "linear"
485
- config["adapters"]["linear"] = linear_config
486
-
487
- elif adapter_type == "jira":
488
- # If not auto-discovered, build from CLI params
489
- if adapter_type not in config["adapters"]:
490
- server = jira_server or os.getenv("JIRA_SERVER")
491
- email = jira_email or os.getenv("JIRA_EMAIL")
492
- token = api_key or os.getenv("JIRA_API_TOKEN")
493
- project = jira_project or os.getenv("JIRA_PROJECT_KEY")
494
-
495
- if not server:
496
- console.print("[red]Error:[/red] JIRA server URL is required")
497
- console.print(
498
- "Use --jira-server or set JIRA_SERVER environment variable"
499
- )
500
- raise typer.Exit(1)
501
-
502
- if not email:
503
- console.print("[red]Error:[/red] JIRA email is required")
504
- console.print("Use --jira-email or set JIRA_EMAIL environment variable")
505
- raise typer.Exit(1)
506
-
507
- if not token:
508
- console.print("[red]Error:[/red] JIRA API token is required")
509
- console.print(
510
- "Use --api-key or set JIRA_API_TOKEN environment variable"
511
- )
512
- console.print(
513
- "[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
514
- )
515
- raise typer.Exit(1)
516
-
517
- jira_config = {"server": server, "email": email, "api_token": token}
518
-
519
- if project:
520
- jira_config["project_key"] = project
521
-
522
- config["adapters"]["jira"] = jira_config
523
-
524
- elif adapter_type == "github":
525
- # If not auto-discovered, build from CLI params
526
- if adapter_type not in config["adapters"]:
527
- owner = github_owner or os.getenv("GITHUB_OWNER")
528
- repo = github_repo or os.getenv("GITHUB_REPO")
529
- token = github_token or os.getenv("GITHUB_TOKEN")
530
-
531
- if not owner:
532
- console.print("[red]Error:[/red] GitHub repository owner is required")
533
- console.print(
534
- "Use --github-owner or set GITHUB_OWNER environment variable"
535
- )
536
- raise typer.Exit(1)
537
-
538
- if not repo:
539
- console.print("[red]Error:[/red] GitHub repository name is required")
540
- console.print(
541
- "Use --github-repo or set GITHUB_REPO environment variable"
542
- )
543
- raise typer.Exit(1)
544
-
545
- if not token:
546
- console.print(
547
- "[red]Error:[/red] GitHub Personal Access Token is required"
548
- )
549
- console.print(
550
- "Use --github-token or set GITHUB_TOKEN environment variable"
551
- )
552
- console.print(
553
- "[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
554
- )
555
- console.print(
556
- "[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]"
557
- )
558
- raise typer.Exit(1)
559
-
560
- config["adapters"]["github"] = {
561
- "owner": owner,
562
- "repo": repo,
563
- "token": token,
564
- }
565
-
566
- # 5. Save to appropriate location
567
- if global_config:
568
- # Save to ~/.mcp-ticketer/config.json
569
- resolver = ConfigResolver(project_path=proj_path)
570
- config_file_path = resolver.GLOBAL_CONFIG_PATH
571
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
572
-
573
- with open(config_file_path, "w") as f:
574
- json.dump(config, f, indent=2)
575
-
576
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
577
- console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
578
- else:
579
- # Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
580
- config_file_path = proj_path / ".mcp-ticketer" / "config.json"
581
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
582
-
583
- with open(config_file_path, "w") as f:
584
- json.dump(config, f, indent=2)
585
-
586
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
587
- console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
588
-
589
- # Add .mcp-ticketer to .gitignore if not already there
590
- gitignore_path = proj_path / ".gitignore"
591
- if gitignore_path.exists():
592
- gitignore_content = gitignore_path.read_text()
593
- if ".mcp-ticketer" not in gitignore_content:
594
- with open(gitignore_path, "a") as f:
595
- f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
596
- console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
597
- else:
598
- # Create .gitignore if it doesn't exist
599
- with open(gitignore_path, "w") as f:
600
- f.write("# MCP Ticketer\n.mcp-ticketer/\n")
601
- console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
602
-
603
-
604
- @app.command()
605
- def install(
606
- adapter: Optional[str] = typer.Option(
607
- None,
608
- "--adapter",
609
- "-a",
610
- help="Adapter type to use (auto-detected from .env if not specified)",
611
- ),
612
- project_path: Optional[str] = typer.Option(
613
- None, "--path", help="Project path (default: current directory)"
614
- ),
615
- global_config: bool = typer.Option(
616
- False,
617
- "--global",
618
- "-g",
619
- help="Save to global config instead of project-specific",
620
- ),
621
- base_path: Optional[str] = typer.Option(
622
- None,
623
- "--base-path",
624
- "-p",
625
- help="Base path for ticket storage (AITrackdown only)",
626
- ),
627
- api_key: Optional[str] = typer.Option(
628
- None, "--api-key", help="API key for Linear or API token for JIRA"
629
- ),
630
- team_id: Optional[str] = typer.Option(
631
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
632
- ),
633
- jira_server: Optional[str] = typer.Option(
634
- None,
635
- "--jira-server",
636
- help="JIRA server URL (e.g., https://company.atlassian.net)",
637
- ),
638
- jira_email: Optional[str] = typer.Option(
639
- None, "--jira-email", help="JIRA user email for authentication"
640
- ),
641
- jira_project: Optional[str] = typer.Option(
642
- None, "--jira-project", help="Default JIRA project key"
643
- ),
644
- github_owner: Optional[str] = typer.Option(
645
- None, "--github-owner", help="GitHub repository owner"
646
- ),
647
- github_repo: Optional[str] = typer.Option(
648
- None, "--github-repo", help="GitHub repository name"
649
- ),
650
- github_token: Optional[str] = typer.Option(
651
- None, "--github-token", help="GitHub Personal Access Token"
652
- ),
653
- ) -> None:
654
- """Initialize mcp-ticketer for the current project (alias for init).
655
-
656
- This command is synonymous with 'init' and provides the same functionality.
657
- Creates .mcp-ticketer/config.json in the current directory with
658
- auto-detected or specified adapter configuration.
659
-
660
- Examples:
661
- # Auto-detect from .env.local
662
- mcp-ticketer install
663
-
664
- # Force specific adapter
665
- mcp-ticketer install --adapter linear
666
-
667
- # Initialize for different project
668
- mcp-ticketer install --path /path/to/project
669
-
670
- # Save globally (not recommended)
671
- mcp-ticketer install --global
672
-
673
- """
674
- # Call init with all parameters
675
- init(
676
- adapter=adapter,
677
- project_path=project_path,
678
- global_config=global_config,
679
- base_path=base_path,
680
- api_key=api_key,
681
- team_id=team_id,
682
- jira_server=jira_server,
683
- jira_email=jira_email,
684
- jira_project=jira_project,
685
- github_owner=github_owner,
686
- github_repo=github_repo,
687
- github_token=github_token,
688
- )
689
-
690
-
691
334
  @app.command("set")
692
335
  def set_config(
693
- adapter: Optional[AdapterType] = typer.Option(
336
+ adapter: AdapterType | None = typer.Option(
694
337
  None, "--adapter", "-a", help="Set default adapter"
695
338
  ),
696
- team_key: Optional[str] = typer.Option(
339
+ team_key: str | None = typer.Option(
697
340
  None, "--team-key", help="Linear team key (e.g., BTA)"
698
341
  ),
699
- team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
700
- owner: Optional[str] = typer.Option(
701
- None, "--owner", help="GitHub repository owner"
702
- ),
703
- repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
704
- server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
705
- project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
706
- base_path: Optional[str] = typer.Option(
342
+ team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
343
+ owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
344
+ repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
345
+ server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
346
+ project: str | None = typer.Option(None, "--project", help="JIRA project key"),
347
+ base_path: str | None = typer.Option(
707
348
  None, "--base-path", help="AITrackdown base path"
708
349
  ),
709
350
  ) -> None:
@@ -793,16 +434,12 @@ def set_config(
793
434
  @app.command("configure")
794
435
  def configure_command(
795
436
  show: bool = typer.Option(False, "--show", help="Show current configuration"),
796
- adapter: Optional[str] = typer.Option(
437
+ adapter: str | None = typer.Option(
797
438
  None, "--adapter", help="Set default adapter type"
798
439
  ),
799
- api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
800
- project_id: Optional[str] = typer.Option(
801
- None, "--project-id", help="Set project ID"
802
- ),
803
- team_id: Optional[str] = typer.Option(
804
- None, "--team-id", help="Set team ID (Linear)"
805
- ),
440
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
441
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
442
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
806
443
  global_scope: bool = typer.Option(
807
444
  False,
808
445
  "--global",
@@ -836,6 +473,26 @@ def configure_command(
836
473
  configure_wizard()
837
474
 
838
475
 
476
+ @app.command("config")
477
+ def config_alias(
478
+ show: bool = typer.Option(False, "--show", help="Show current configuration"),
479
+ adapter: str | None = typer.Option(
480
+ None, "--adapter", help="Set default adapter type"
481
+ ),
482
+ api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
483
+ project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
484
+ team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
485
+ global_scope: bool = typer.Option(
486
+ False,
487
+ "--global",
488
+ "-g",
489
+ help="Save to global config instead of project-specific",
490
+ ),
491
+ ) -> None:
492
+ """Alias for configure command - shorter syntax."""
493
+ configure_command(show, adapter, api_key, project_id, team_id, global_scope)
494
+
495
+
839
496
  @app.command("migrate-config")
840
497
  def migrate_config(
841
498
  dry_run: bool = typer.Option(
@@ -853,630 +510,74 @@ def migrate_config(
853
510
  migrate_config_command(dry_run=dry_run)
854
511
 
855
512
 
856
- @app.command("status")
857
- def status_command():
858
- """Show queue and worker status."""
859
- queue = Queue()
860
- manager = WorkerManager()
861
-
862
- # Get queue stats
863
- stats = queue.get_stats()
864
- pending = stats.get(QueueStatus.PENDING.value, 0)
865
-
866
- # Show queue status
867
- console.print("[bold]Queue Status:[/bold]")
868
- console.print(f" Pending: {pending}")
869
- console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
870
- console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
871
- console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
872
-
873
- # Show worker status
874
- worker_status = manager.get_status()
875
- if worker_status["running"]:
876
- console.print(
877
- f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
878
- )
879
- else:
880
- console.print("\n[red]○ Worker is not running[/red]")
881
- if pending > 0:
882
- console.print(
883
- "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
884
- )
885
-
886
-
887
- @app.command()
888
- def health(
889
- auto_repair: bool = typer.Option(False, "--auto-repair", help="Attempt automatic repair of issues"),
890
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed health information")
891
- ) -> None:
892
- """Check queue system health and detect issues immediately."""
893
-
894
- health_monitor = QueueHealthMonitor()
895
- health = health_monitor.check_health()
896
-
897
- # Display overall status
898
- status_color = {
899
- HealthStatus.HEALTHY: "green",
900
- HealthStatus.WARNING: "yellow",
901
- HealthStatus.CRITICAL: "red",
902
- HealthStatus.FAILED: "red"
903
- }
904
-
905
- status_icon = {
906
- HealthStatus.HEALTHY: "✓",
907
- HealthStatus.WARNING: "⚠️",
908
- HealthStatus.CRITICAL: "🚨",
909
- HealthStatus.FAILED: "❌"
910
- }
911
-
912
- color = status_color.get(health["status"], "white")
913
- icon = status_icon.get(health["status"], "?")
914
-
915
- console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
916
- console.print(f"Last checked: {health['timestamp']}")
917
-
918
- # Display alerts
919
- if health["alerts"]:
920
- console.print("\n[bold]Issues Found:[/bold]")
921
- for alert in health["alerts"]:
922
- alert_color = status_color.get(alert["level"], "white")
923
- console.print(f"[{alert_color}] • {alert['message']}[/{alert_color}]")
924
-
925
- if verbose and alert.get("details"):
926
- for key, value in alert["details"].items():
927
- console.print(f" {key}: {value}")
928
- else:
929
- console.print("\n[green]✓ No issues detected[/green]")
930
-
931
- # Auto-repair if requested
932
- if auto_repair and health["status"] in [HealthStatus.CRITICAL, HealthStatus.WARNING]:
933
- console.print("\n[yellow]Attempting automatic repair...[/yellow]")
934
- repair_result = health_monitor.auto_repair()
935
-
936
- if repair_result["actions_taken"]:
937
- console.print("[green]Repair actions taken:[/green]")
938
- for action in repair_result["actions_taken"]:
939
- console.print(f"[green] ✓ {action}[/green]")
940
-
941
- # Re-check health
942
- console.print("\n[yellow]Re-checking health after repair...[/yellow]")
943
- new_health = health_monitor.check_health()
944
- new_color = status_color.get(new_health["status"], "white")
945
- new_icon = status_icon.get(new_health["status"], "?")
946
- console.print(f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]")
947
- else:
948
- console.print("[yellow]No repair actions available[/yellow]")
949
-
950
- # Exit with appropriate code
951
- if health["status"] == HealthStatus.CRITICAL:
952
- raise typer.Exit(1)
953
- elif health["status"] == HealthStatus.WARNING:
954
- raise typer.Exit(2)
955
-
956
-
957
- @app.command()
958
- def create(
959
- title: str = typer.Argument(..., help="Ticket title"),
960
- description: Optional[str] = typer.Option(
961
- None, "--description", "-d", help="Ticket description"
962
- ),
963
- priority: Priority = typer.Option(
964
- Priority.MEDIUM, "--priority", "-p", help="Priority level"
965
- ),
966
- tags: Optional[list[str]] = typer.Option(
967
- None, "--tag", "-t", help="Tags (can be specified multiple times)"
968
- ),
969
- assignee: Optional[str] = typer.Option(
970
- None, "--assignee", "-a", help="Assignee username"
971
- ),
972
- adapter: Optional[AdapterType] = typer.Option(
973
- None, "--adapter", help="Override default adapter"
974
- ),
975
- ) -> None:
976
- """Create a new ticket with comprehensive health checks."""
977
-
978
- # IMMEDIATE HEALTH CHECK - Critical for reliability
979
- health_monitor = QueueHealthMonitor()
980
- health = health_monitor.check_health()
981
-
982
- # Display health status
983
- if health["status"] == HealthStatus.CRITICAL:
984
- console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
985
- for alert in health["alerts"]:
986
- if alert["level"] == "critical":
987
- console.print(f"[red] • {alert['message']}[/red]")
988
-
989
- # Attempt auto-repair
990
- console.print("[yellow]Attempting automatic repair...[/yellow]")
991
- repair_result = health_monitor.auto_repair()
992
-
993
- if repair_result["actions_taken"]:
994
- for action in repair_result["actions_taken"]:
995
- console.print(f"[yellow] ✓ {action}[/yellow]")
996
-
997
- # Re-check health after repair
998
- health = health_monitor.check_health()
999
- if health["status"] == HealthStatus.CRITICAL:
1000
- console.print("[red]❌ Auto-repair failed. Manual intervention required.[/red]")
1001
- console.print("[red]Cannot safely create ticket. Please check system status.[/red]")
1002
- raise typer.Exit(1)
1003
- else:
1004
- console.print("[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]")
1005
- else:
1006
- console.print("[red]❌ No repair actions available. Manual intervention required.[/red]")
1007
- raise typer.Exit(1)
1008
-
1009
- elif health["status"] == HealthStatus.WARNING:
1010
- console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
1011
- for alert in health["alerts"]:
1012
- if alert["level"] == "warning":
1013
- console.print(f"[yellow] • {alert['message']}[/yellow]")
1014
- console.print("[yellow]Proceeding with ticket creation...[/yellow]")
1015
-
1016
- # Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
1017
- if adapter:
1018
- # Priority 1: Command-line argument - save to config for future use
1019
- adapter_name = adapter.value
1020
- _save_adapter_to_config(adapter_name)
1021
- else:
1022
- # Priority 2: Check existing config
1023
- config = load_config()
1024
- adapter_name = config.get("default_adapter")
1025
-
1026
- if not adapter_name or adapter_name == "aitrackdown":
1027
- # Priority 3: Check .env files and save if found
1028
- env_adapter = _discover_from_env_files()
1029
- if env_adapter:
1030
- adapter_name = env_adapter
1031
- _save_adapter_to_config(adapter_name)
1032
- else:
1033
- # Priority 4: Default
1034
- adapter_name = "aitrackdown"
1035
-
1036
- # Create task data
1037
- # Import Priority for type checking
1038
- from ..core.models import Priority as PriorityEnum
1039
-
1040
- task_data = {
1041
- "title": title,
1042
- "description": description,
1043
- "priority": priority.value if isinstance(priority, PriorityEnum) else priority,
1044
- "tags": tags or [],
1045
- "assignee": assignee,
1046
- }
1047
-
1048
- # WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
1049
- if adapter_name == "linear":
1050
- console.print("[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)")
1051
- try:
1052
- # Load configuration and create adapter directly
1053
- config = load_config()
1054
- adapter_config = config.get("adapters", {}).get(adapter_name, {})
1055
-
1056
- # Import and create adapter
1057
- from ..core.registry import AdapterRegistry
1058
- adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
1059
-
1060
- # Create task directly
1061
- from ..core.models import Task, Priority
1062
- task = Task(
1063
- title=task_data["title"],
1064
- description=task_data.get("description"),
1065
- priority=Priority(task_data["priority"]) if task_data.get("priority") else Priority.MEDIUM,
1066
- tags=task_data.get("tags", []),
1067
- assignee=task_data.get("assignee")
1068
- )
1069
-
1070
- # Create ticket synchronously
1071
- import asyncio
1072
- result = asyncio.run(adapter.create(task))
1073
-
1074
- console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
1075
- console.print(f" Title: {result.title}")
1076
- console.print(f" Priority: {result.priority}")
1077
- console.print(f" State: {result.state}")
1078
- # Get URL from metadata if available
1079
- if result.metadata and 'linear' in result.metadata and 'url' in result.metadata['linear']:
1080
- console.print(f" URL: {result.metadata['linear']['url']}")
513
+ # Add ticket command group to main app
514
+ app.add_typer(ticket_app, name="ticket")
1081
515
 
1082
- return result.id
1083
-
1084
- except Exception as e:
1085
- console.print(f"[red]❌[/red] Failed to create ticket: {e}")
1086
- raise
1087
-
1088
- # Use queue for other adapters
1089
- queue = Queue()
1090
- queue_id = queue.add(
1091
- ticket_data=task_data,
1092
- adapter=adapter_name,
1093
- operation="create",
1094
- project_dir=str(Path.cwd()) # Explicitly pass current project directory
1095
- )
1096
-
1097
- # Register in ticket registry for tracking
1098
- registry = TicketRegistry()
1099
- registry.register_ticket_operation(queue_id, adapter_name, "create", title, task_data)
1100
-
1101
- console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
1102
- console.print(f" Title: {title}")
1103
- console.print(f" Priority: {priority}")
1104
- console.print(f" Adapter: {adapter_name}")
1105
- console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
1106
-
1107
- # Start worker if needed with immediate feedback
1108
- manager = WorkerManager()
1109
- worker_started = manager.start_if_needed()
1110
-
1111
- if worker_started:
1112
- console.print("[dim]Worker started to process request[/dim]")
1113
-
1114
- # Give immediate feedback on processing
1115
- import time
1116
- time.sleep(1) # Brief pause to let worker start
1117
-
1118
- # Check if item is being processed
1119
- item = queue.get_item(queue_id)
1120
- if item and item.status == QueueStatus.PROCESSING:
1121
- console.print("[green]✓ Item is being processed by worker[/green]")
1122
- elif item and item.status == QueueStatus.PENDING:
1123
- console.print("[yellow]⏳ Item is queued for processing[/yellow]")
1124
- else:
1125
- console.print("[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]")
1126
- else:
1127
- # Worker didn't start - this is a problem
1128
- pending_count = queue.get_pending_count()
1129
- if pending_count > 1: # More than just this item
1130
- console.print(f"[red]❌ Worker failed to start with {pending_count} pending items![/red]")
1131
- console.print("[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]")
1132
- else:
1133
- console.print("[yellow]Worker not started (no other pending items)[/yellow]")
1134
-
1135
-
1136
- @app.command("list")
1137
- def list_tickets(
1138
- state: Optional[TicketState] = typer.Option(
1139
- None, "--state", "-s", help="Filter by state"
1140
- ),
1141
- priority: Optional[Priority] = typer.Option(
1142
- None, "--priority", "-p", help="Filter by priority"
1143
- ),
1144
- limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
1145
- adapter: Optional[AdapterType] = typer.Option(
1146
- None, "--adapter", help="Override default adapter"
1147
- ),
1148
- ) -> None:
1149
- """List tickets with optional filters."""
1150
-
1151
- async def _list():
1152
- adapter_instance = get_adapter(
1153
- override_adapter=adapter.value if adapter else None
1154
- )
1155
- filters = {}
1156
- if state:
1157
- filters["state"] = state
1158
- if priority:
1159
- filters["priority"] = priority
1160
- return await adapter_instance.list(limit=limit, filters=filters)
1161
-
1162
- tickets = asyncio.run(_list())
1163
-
1164
- if not tickets:
1165
- console.print("[yellow]No tickets found[/yellow]")
1166
- return
1167
-
1168
- # Create table
1169
- table = Table(title="Tickets")
1170
- table.add_column("ID", style="cyan", no_wrap=True)
1171
- table.add_column("Title", style="white")
1172
- table.add_column("State", style="green")
1173
- table.add_column("Priority", style="yellow")
1174
- table.add_column("Assignee", style="blue")
1175
-
1176
- for ticket in tickets:
1177
- # Handle assignee field - Epic doesn't have assignee, Task does
1178
- assignee = getattr(ticket, 'assignee', None) or "-"
1179
-
1180
- table.add_row(
1181
- ticket.id or "N/A",
1182
- ticket.title,
1183
- ticket.state,
1184
- ticket.priority,
1185
- assignee,
1186
- )
1187
-
1188
- console.print(table)
516
+ # Add platform command group to main app
517
+ app.add_typer(platform_app, name="platform")
1189
518
 
519
+ # Add queue command to main app
520
+ app.add_typer(queue_app, name="queue")
1190
521
 
1191
- @app.command()
1192
- def show(
1193
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1194
- comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
1195
- adapter: Optional[AdapterType] = typer.Option(
1196
- None, "--adapter", help="Override default adapter"
1197
- ),
1198
- ) -> None:
1199
- """Show detailed ticket information."""
522
+ # Add discover command to main app
523
+ app.add_typer(discover_app, name="discover")
1200
524
 
1201
- async def _show():
1202
- adapter_instance = get_adapter(
1203
- override_adapter=adapter.value if adapter else None
1204
- )
1205
- ticket = await adapter_instance.read(ticket_id)
1206
- ticket_comments = None
1207
- if comments and ticket:
1208
- ticket_comments = await adapter_instance.get_comments(ticket_id)
1209
- return ticket, ticket_comments
1210
-
1211
- ticket, ticket_comments = asyncio.run(_show())
1212
-
1213
- if not ticket:
1214
- console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
1215
- raise typer.Exit(1)
1216
-
1217
- # Display ticket details
1218
- console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
1219
- console.print(f"Title: {ticket.title}")
1220
- console.print(f"State: [green]{ticket.state}[/green]")
1221
- console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
1222
-
1223
- if ticket.description:
1224
- console.print("\n[dim]Description:[/dim]")
1225
- console.print(ticket.description)
1226
-
1227
- if ticket.tags:
1228
- console.print(f"\nTags: {', '.join(ticket.tags)}")
1229
-
1230
- if ticket.assignee:
1231
- console.print(f"Assignee: {ticket.assignee}")
1232
-
1233
- # Display comments if requested
1234
- if ticket_comments:
1235
- console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
1236
- for comment in ticket_comments:
1237
- console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
1238
- console.print(comment.content)
1239
-
1240
-
1241
- @app.command()
1242
- def comment(
1243
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1244
- content: str = typer.Argument(..., help="Comment content"),
1245
- adapter: Optional[AdapterType] = typer.Option(
1246
- None, "--adapter", help="Override default adapter"
1247
- ),
1248
- ) -> None:
1249
- """Add a comment to a ticket."""
525
+ # Add instructions command to main app
526
+ app.add_typer(instruction_app, name="instructions")
1250
527
 
1251
- async def _comment():
1252
- adapter_instance = get_adapter(
1253
- override_adapter=adapter.value if adapter else None
1254
- )
528
+ # Add project-update command group to main app
529
+ app.add_typer(project_update_app, name="project-update")
1255
530
 
1256
- # Create comment
1257
- comment = Comment(
1258
- ticket_id=ticket_id,
1259
- content=content,
1260
- author="cli-user" # Could be made configurable
1261
- )
531
+ # Add setup and init commands to main app
532
+ app.command()(setup)
533
+ app.command()(init)
1262
534
 
1263
- result = await adapter_instance.add_comment(comment)
1264
- return result
535
+ # Add platform installer commands to main app
536
+ app.command()(install)
537
+ app.command()(remove)
538
+ app.command()(uninstall)
1265
539
 
1266
- try:
1267
- result = asyncio.run(_comment())
1268
- console.print(f"[green]✓[/green] Comment added successfully")
1269
- if result.id:
1270
- console.print(f"Comment ID: {result.id}")
1271
- console.print(f"Content: {content}")
1272
- except Exception as e:
1273
- console.print(f"[red]✗[/red] Failed to add comment: {e}")
1274
- raise typer.Exit(1)
540
+ # Add MCP server installer commands
541
+ app.command(name="install-mcp-server")(install_mcp_server)
542
+ app.command(name="list-mcp-servers")(list_mcp_servers)
543
+ app.command(name="uninstall-mcp-server")(uninstall_mcp_server)
1275
544
 
1276
545
 
1277
- @app.command()
1278
- def update(
1279
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1280
- title: Optional[str] = typer.Option(None, "--title", help="New title"),
1281
- description: Optional[str] = typer.Option(
1282
- None, "--description", "-d", help="New description"
1283
- ),
1284
- priority: Optional[Priority] = typer.Option(
1285
- None, "--priority", "-p", help="New priority"
1286
- ),
1287
- assignee: Optional[str] = typer.Option(
1288
- None, "--assignee", "-a", help="New assignee"
1289
- ),
1290
- adapter: Optional[AdapterType] = typer.Option(
1291
- None, "--adapter", help="Override default adapter"
1292
- ),
1293
- ) -> None:
1294
- """Update ticket fields."""
1295
- updates = {}
1296
- if title:
1297
- updates["title"] = title
1298
- if description:
1299
- updates["description"] = description
1300
- if priority:
1301
- updates["priority"] = (
1302
- priority.value if isinstance(priority, Priority) else priority
1303
- )
1304
- if assignee:
1305
- updates["assignee"] = assignee
1306
-
1307
- if not updates:
1308
- console.print("[yellow]No updates specified[/yellow]")
1309
- raise typer.Exit(1)
1310
-
1311
- # Get the adapter name
1312
- config = load_config()
1313
- adapter_name = (
1314
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1315
- )
1316
-
1317
- # Add ticket_id to updates
1318
- updates["ticket_id"] = ticket_id
1319
-
1320
- # Add to queue with explicit project directory
1321
- queue = Queue()
1322
- queue_id = queue.add(
1323
- ticket_data=updates,
1324
- adapter=adapter_name,
1325
- operation="update",
1326
- project_dir=str(Path.cwd()) # Explicitly pass current project directory
1327
- )
1328
-
1329
- console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
1330
- for key, value in updates.items():
1331
- if key != "ticket_id":
1332
- console.print(f" {key}: {value}")
1333
- console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
1334
-
1335
- # Start worker if needed
1336
- manager = WorkerManager()
1337
- if manager.start_if_needed():
1338
- console.print("[dim]Worker started to process request[/dim]")
1339
-
1340
-
1341
- @app.command()
1342
- def transition(
1343
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1344
- state_positional: Optional[TicketState] = typer.Argument(
1345
- None, help="Target state (positional - deprecated, use --state instead)"
546
+ # Add diagnostics command
547
+ @app.command("doctor")
548
+ def doctor_command(
549
+ output_file: str | None = typer.Option(
550
+ None, "--output", "-o", help="Save full report to file"
1346
551
  ),
1347
- state: Optional[TicketState] = typer.Option(
1348
- None, "--state", "-s", help="Target state (recommended)"
552
+ json_output: bool = typer.Option(
553
+ False, "--json", help="Output report in JSON format"
1349
554
  ),
1350
- adapter: Optional[AdapterType] = typer.Option(
1351
- None, "--adapter", help="Override default adapter"
555
+ simple: bool = typer.Option(
556
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1352
557
  ),
1353
558
  ) -> None:
1354
- """Change ticket state with validation.
1355
-
1356
- Examples:
1357
- # Recommended syntax with flag:
1358
- mcp-ticketer transition BTA-215 --state done
1359
- mcp-ticketer transition BTA-215 -s in_progress
1360
-
1361
- # Legacy positional syntax (still supported):
1362
- mcp-ticketer transition BTA-215 done
1363
-
1364
- """
1365
- # Determine which state to use (prefer flag over positional)
1366
- target_state = state if state is not None else state_positional
1367
-
1368
- if target_state is None:
1369
- console.print("[red]Error: State is required[/red]")
1370
- console.print(
1371
- "Use either:\n"
1372
- " - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
1373
- " - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
1374
- )
1375
- raise typer.Exit(1)
1376
-
1377
- # Get the adapter name
1378
- config = load_config()
1379
- adapter_name = (
1380
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1381
- )
1382
-
1383
- # Add to queue with explicit project directory
1384
- queue = Queue()
1385
- queue_id = queue.add(
1386
- ticket_data={
1387
- "ticket_id": ticket_id,
1388
- "state": (
1389
- target_state.value if hasattr(target_state, "value") else target_state
1390
- ),
1391
- },
1392
- adapter=adapter_name,
1393
- operation="transition",
1394
- project_dir=str(Path.cwd()) # Explicitly pass current project directory
1395
- )
1396
-
1397
- console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
1398
- console.print(f" Ticket: {ticket_id} → {target_state}")
1399
- console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
1400
-
1401
- # Start worker if needed
1402
- manager = WorkerManager()
1403
- if manager.start_if_needed():
1404
- console.print("[dim]Worker started to process request[/dim]")
1405
-
1406
-
1407
- @app.command()
1408
- def search(
1409
- query: Optional[str] = typer.Argument(None, help="Search query"),
1410
- state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
1411
- priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
1412
- assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
1413
- limit: int = typer.Option(10, "--limit", "-l"),
1414
- adapter: Optional[AdapterType] = typer.Option(
1415
- None, "--adapter", help="Override default adapter"
1416
- ),
1417
- ) -> None:
1418
- """Search tickets with advanced query."""
1419
-
1420
- async def _search():
1421
- adapter_instance = get_adapter(
1422
- override_adapter=adapter.value if adapter else None
1423
- )
1424
- search_query = SearchQuery(
1425
- query=query,
1426
- state=state,
1427
- priority=priority,
1428
- assignee=assignee,
1429
- limit=limit,
1430
- )
1431
- return await adapter_instance.search(search_query)
1432
-
1433
- tickets = asyncio.run(_search())
1434
-
1435
- if not tickets:
1436
- console.print("[yellow]No tickets found matching query[/yellow]")
1437
- return
1438
-
1439
- # Display results
1440
- console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
1441
-
1442
- for ticket in tickets:
1443
- console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
1444
- console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
1445
- if ticket.assignee:
1446
- console.print(f" Assignee: {ticket.assignee}")
1447
- console.print()
1448
-
1449
-
1450
- # Add queue command to main app
1451
- app.add_typer(queue_app, name="queue")
1452
-
1453
- # Add discover command to main app
1454
- app.add_typer(discover_app, name="discover")
1455
-
1456
- # Add diagnostics command
1457
- @app.command()
1458
- def diagnose(
1459
- output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Save full report to file"),
1460
- json_output: bool = typer.Option(False, "--json", help="Output report in JSON format"),
1461
- simple: bool = typer.Option(False, "--simple", help="Use simple diagnostics (no heavy dependencies)"),
1462
- ) -> None:
1463
- """Run comprehensive system diagnostics and health check."""
559
+ """Run comprehensive system diagnostics and health check (alias: diagnose)."""
1464
560
  if simple:
1465
561
  from .simple_health import simple_diagnose
562
+
1466
563
  report = simple_diagnose()
1467
564
  if output_file:
1468
565
  import json
1469
- with open(output_file, 'w') as f:
566
+
567
+ with open(output_file, "w") as f:
1470
568
  json.dump(report, f, indent=2)
1471
569
  console.print(f"\n📄 Report saved to: {output_file}")
1472
570
  if json_output:
1473
571
  import json
572
+
1474
573
  console.print("\n" + json.dumps(report, indent=2))
1475
574
  if report["issues"]:
1476
- raise typer.Exit(1)
575
+ raise typer.Exit(1) from None
1477
576
  else:
1478
577
  try:
1479
- asyncio.run(run_diagnostics(output_file=output_file, json_output=json_output))
578
+ asyncio.run(
579
+ run_diagnostics(output_file=output_file, json_output=json_output)
580
+ )
1480
581
  except typer.Exit:
1481
582
  # typer.Exit is expected - don't fall back to simple diagnostics
1482
583
  raise
@@ -1484,300 +585,55 @@ def diagnose(
1484
585
  console.print(f"⚠️ Full diagnostics failed: {e}")
1485
586
  console.print("🔄 Falling back to simple diagnostics...")
1486
587
  from .simple_health import simple_diagnose
588
+
1487
589
  report = simple_diagnose()
1488
590
  if report["issues"]:
1489
- raise typer.Exit(1)
1490
-
1491
-
1492
- @app.command()
1493
- def health() -> None:
1494
- """Quick health check - shows system status summary."""
1495
- from .simple_health import simple_health_check
1496
-
1497
- result = simple_health_check()
1498
- if result != 0:
1499
- raise typer.Exit(result)
1500
-
1501
- # Create MCP configuration command group
1502
- mcp_app = typer.Typer(
1503
- name="mcp",
1504
- help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
1505
- add_completion=False,
1506
- )
1507
-
1508
-
1509
- @app.command()
1510
- def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
1511
- """Check status of a queued operation."""
1512
- queue = Queue()
1513
- item = queue.get_item(queue_id)
1514
-
1515
- if not item:
1516
- console.print(f"[red]Queue item not found: {queue_id}[/red]")
1517
- raise typer.Exit(1)
1518
-
1519
- # Display status
1520
- console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
1521
- console.print(f"Operation: {item.operation}")
1522
- console.print(f"Adapter: {item.adapter}")
1523
-
1524
- # Status with color
1525
- if item.status == QueueStatus.COMPLETED:
1526
- console.print(f"Status: [green]{item.status}[/green]")
1527
- elif item.status == QueueStatus.FAILED:
1528
- console.print(f"Status: [red]{item.status}[/red]")
1529
- elif item.status == QueueStatus.PROCESSING:
1530
- console.print(f"Status: [yellow]{item.status}[/yellow]")
1531
- else:
1532
- console.print(f"Status: {item.status}")
1533
-
1534
- # Timestamps
1535
- console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
1536
- if item.processed_at:
1537
- console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
591
+ raise typer.Exit(1) from None
1538
592
 
1539
- # Error or result
1540
- if item.error_message:
1541
- console.print(f"\n[red]Error:[/red] {item.error_message}")
1542
- elif item.result:
1543
- console.print("\n[green]Result:[/green]")
1544
- for key, value in item.result.items():
1545
- console.print(f" {key}: {value}")
1546
593
 
1547
- if item.retry_count > 0:
1548
- console.print(f"\nRetry Count: {item.retry_count}")
1549
-
1550
-
1551
- @app.command()
1552
- def serve(
1553
- adapter: Optional[AdapterType] = typer.Option(
1554
- None, "--adapter", "-a", help="Override default adapter type"
594
+ @app.command("diagnose", hidden=True)
595
+ def diagnose_alias(
596
+ output_file: str | None = typer.Option(
597
+ None, "--output", "-o", help="Save full report to file"
1555
598
  ),
1556
- base_path: Optional[str] = typer.Option(
1557
- None, "--base-path", help="Base path for AITrackdown adapter"
599
+ json_output: bool = typer.Option(
600
+ False, "--json", help="Output report in JSON format"
1558
601
  ),
1559
- ):
1560
- """Start MCP server for JSON-RPC communication over stdio.
1561
-
1562
- This command is used by Claude Code/Desktop when connecting to the MCP server.
1563
- You typically don't need to run this manually - use 'mcp-ticketer mcp' to configure.
1564
-
1565
- Configuration Resolution:
1566
- - When MCP server starts, it uses the current working directory (cwd)
1567
- - The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
1568
- - Configuration is loaded with this priority:
1569
- 1. Project-specific: .mcp-ticketer/config.json in cwd
1570
- 2. Global: ~/.mcp-ticketer/config.json
1571
- 3. Default: aitrackdown adapter with .aitrackdown base path
1572
- """
1573
- from ..mcp.server import MCPTicketServer
1574
-
1575
- # Load configuration (respects project-specific config in cwd)
1576
- config = load_config()
1577
-
1578
- # Determine adapter type
1579
- adapter_type = (
1580
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1581
- )
1582
-
1583
- # Get adapter configuration
1584
- adapters_config = config.get("adapters", {})
1585
- adapter_config = adapters_config.get(adapter_type, {})
1586
-
1587
- # Override with command line options if provided
1588
- if base_path and adapter_type == "aitrackdown":
1589
- adapter_config["base_path"] = base_path
1590
-
1591
- # Fallback to legacy config format
1592
- if not adapter_config and "config" in config:
1593
- adapter_config = config["config"]
1594
-
1595
- # MCP server uses stdio for JSON-RPC, so we can't print to stdout
1596
- # Only print to stderr to avoid interfering with the protocol
1597
- import sys
1598
-
1599
- if sys.stderr.isatty():
1600
- # Only print if stderr is a terminal (not redirected)
1601
- console.file = sys.stderr
1602
- console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
1603
- console.print(
1604
- "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
1605
- )
1606
-
1607
- # Create and run server
1608
- try:
1609
- server = MCPTicketServer(adapter_type, adapter_config)
1610
- asyncio.run(server.run())
1611
- except KeyboardInterrupt:
1612
- # Also send this to stderr
1613
- if sys.stderr.isatty():
1614
- console.print("\n[yellow]Server stopped by user[/yellow]")
1615
- if "server" in locals():
1616
- asyncio.run(server.stop())
1617
- except Exception as e:
1618
- # Log error to stderr
1619
- sys.stderr.write(f"MCP server error: {e}\n")
1620
- sys.exit(1)
1621
-
1622
-
1623
- @mcp_app.command(name="claude")
1624
- def mcp_claude(
1625
- global_config: bool = typer.Option(
1626
- False,
1627
- "--global",
1628
- "-g",
1629
- help="Configure Claude Desktop instead of project-level",
602
+ simple: bool = typer.Option(
603
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1630
604
  ),
1631
- force: bool = typer.Option(
1632
- False, "--force", "-f", help="Overwrite existing configuration"
1633
- ),
1634
- ):
1635
- """Configure Claude Code to use mcp-ticketer MCP server.
1636
-
1637
- Reads configuration from .mcp-ticketer/config.json and updates
1638
- Claude Code's MCP settings accordingly.
1639
-
1640
- By default, configures project-level (.mcp/config.json).
1641
- Use --global to configure Claude Desktop instead.
1642
-
1643
- Examples:
1644
- # Configure for current project (default)
1645
- mcp-ticketer mcp claude
1646
-
1647
- # Configure Claude Desktop globally
1648
- mcp-ticketer mcp claude --global
1649
-
1650
- # Force overwrite existing configuration
1651
- mcp-ticketer mcp claude --force
1652
-
1653
- """
1654
- from ..cli.mcp_configure import configure_claude_mcp
1655
-
1656
- try:
1657
- configure_claude_mcp(global_config=global_config, force=force)
1658
- except Exception as e:
1659
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1660
- raise typer.Exit(1)
1661
-
1662
-
1663
- @mcp_app.command(name="gemini")
1664
- def mcp_gemini(
1665
- scope: str = typer.Option(
1666
- "project",
1667
- "--scope",
1668
- "-s",
1669
- help="Configuration scope: 'project' (default) or 'user'",
1670
- ),
1671
- force: bool = typer.Option(
1672
- False, "--force", "-f", help="Overwrite existing configuration"
1673
- ),
1674
- ):
1675
- """Configure Gemini CLI to use mcp-ticketer MCP server.
1676
-
1677
- Reads configuration from .mcp-ticketer/config.json and creates
1678
- Gemini CLI settings file with mcp-ticketer configuration.
1679
-
1680
- By default, configures project-level (.gemini/settings.json).
1681
- Use --scope user to configure user-level (~/.gemini/settings.json).
1682
-
1683
- Examples:
1684
- # Configure for current project (default)
1685
- mcp-ticketer mcp gemini
1686
-
1687
- # Configure at user level
1688
- mcp-ticketer mcp gemini --scope user
1689
-
1690
- # Force overwrite existing configuration
1691
- mcp-ticketer mcp gemini --force
1692
-
1693
- """
1694
- from ..cli.gemini_configure import configure_gemini_mcp
1695
-
1696
- # Validate scope parameter
1697
- if scope not in ["project", "user"]:
1698
- console.print(
1699
- f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
1700
- )
1701
- raise typer.Exit(1)
1702
-
1703
- try:
1704
- configure_gemini_mcp(scope=scope, force=force) # type: ignore
1705
- except Exception as e:
1706
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1707
- raise typer.Exit(1)
1708
-
1709
-
1710
- @mcp_app.command(name="codex")
1711
- def mcp_codex(
1712
- force: bool = typer.Option(
1713
- False, "--force", "-f", help="Overwrite existing configuration"
1714
- ),
1715
- ):
1716
- """Configure Codex CLI to use mcp-ticketer MCP server.
1717
-
1718
- Reads configuration from .mcp-ticketer/config.json and creates
1719
- Codex CLI config.toml with mcp-ticketer configuration.
1720
-
1721
- IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
1722
- There is no project-level configuration support. After configuration,
1723
- you must restart Codex CLI for changes to take effect.
1724
-
1725
- Examples:
1726
- # Configure Codex CLI globally
1727
- mcp-ticketer mcp codex
1728
-
1729
- # Force overwrite existing configuration
1730
- mcp-ticketer mcp codex --force
1731
-
1732
- """
1733
- from ..cli.codex_configure import configure_codex_mcp
1734
-
1735
- try:
1736
- configure_codex_mcp(force=force)
1737
- except Exception as e:
1738
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1739
- raise typer.Exit(1)
1740
-
1741
-
1742
- @mcp_app.command(name="auggie")
1743
- def mcp_auggie(
1744
- force: bool = typer.Option(
1745
- False, "--force", "-f", help="Overwrite existing configuration"
1746
- ),
1747
- ):
1748
- """Configure Auggie CLI to use mcp-ticketer MCP server.
605
+ ) -> None:
606
+ """Run comprehensive system diagnostics and health check (alias for doctor)."""
607
+ # Call the doctor_command function with the same parameters
608
+ doctor_command(output_file=output_file, json_output=json_output, simple=simple)
1749
609
 
1750
- Reads configuration from .mcp-ticketer/config.json and creates
1751
- Auggie CLI settings.json with mcp-ticketer configuration.
1752
610
 
1753
- IMPORTANT: Auggie CLI ONLY supports global configuration at ~/.augment/settings.json.
1754
- There is no project-level configuration support. After configuration,
1755
- you must restart Auggie CLI for changes to take effect.
611
+ @app.command("status")
612
+ def status_command() -> None:
613
+ """Quick health check - shows system status summary (alias: health)."""
614
+ from .simple_health import simple_health_check
1756
615
 
1757
- Examples:
1758
- # Configure Auggie CLI globally
1759
- mcp-ticketer mcp auggie
616
+ result = simple_health_check()
617
+ if result != 0:
618
+ raise typer.Exit(result) from None
1760
619
 
1761
- # Force overwrite existing configuration
1762
- mcp-ticketer mcp auggie --force
1763
620
 
1764
- """
1765
- from ..cli.auggie_configure import configure_auggie_mcp
621
+ @app.command("health")
622
+ def health_alias() -> None:
623
+ """Quick health check - shows system status summary (alias for status)."""
624
+ from .simple_health import simple_health_check
1766
625
 
1767
- try:
1768
- configure_auggie_mcp(force=force)
1769
- except Exception as e:
1770
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1771
- raise typer.Exit(1)
626
+ result = simple_health_check()
627
+ if result != 0:
628
+ raise typer.Exit(result) from None
1772
629
 
1773
630
 
1774
631
  # Add command groups to main app (must be after all subcommands are defined)
1775
- app.add_typer(linear_app, name="linear")
1776
632
  app.add_typer(mcp_app, name="mcp")
1777
633
 
1778
634
 
1779
- def main():
1780
- """Main entry point."""
635
+ def main() -> None:
636
+ """Execute the main CLI application entry point."""
1781
637
  app()
1782
638
 
1783
639