mcp-ticketer 0.3.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 +91 -54
  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 -1544
  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 -2030
  155. mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
  157. mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.3.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,639 +331,20 @@ def get_adapter(
317
331
  return AdapterRegistry.get_adapter(adapter_type, adapter_config)
318
332
 
319
333
 
320
- def _prompt_for_adapter_selection(console: Console) -> str:
321
- """Interactive prompt for adapter selection.
322
-
323
- Args:
324
- console: Rich console for output
325
-
326
- Returns:
327
- Selected adapter type
328
- """
329
- console.print("\n[bold blue]🚀 MCP Ticketer Setup[/bold blue]")
330
- console.print("Choose which ticket system you want to connect to:\n")
331
-
332
- # Define adapter options with descriptions
333
- adapters = [
334
- {
335
- "name": "linear",
336
- "title": "Linear",
337
- "description": "Modern project management (linear.app)",
338
- "requirements": "API key and team ID"
339
- },
340
- {
341
- "name": "github",
342
- "title": "GitHub Issues",
343
- "description": "GitHub repository issues",
344
- "requirements": "Personal access token, owner, and repo"
345
- },
346
- {
347
- "name": "jira",
348
- "title": "JIRA",
349
- "description": "Atlassian JIRA project management",
350
- "requirements": "Server URL, email, and API token"
351
- },
352
- {
353
- "name": "aitrackdown",
354
- "title": "Local Files (AITrackdown)",
355
- "description": "Store tickets in local files (no external service)",
356
- "requirements": "None - works offline"
357
- }
358
- ]
359
-
360
- # Display options
361
- for i, adapter in enumerate(adapters, 1):
362
- console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
363
- console.print(f" {adapter['description']}")
364
- console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
365
-
366
- # Get user selection
367
- while True:
368
- try:
369
- choice = typer.prompt(
370
- "Select adapter (1-4)",
371
- type=int,
372
- default=1
373
- )
374
- if 1 <= choice <= len(adapters):
375
- selected_adapter = adapters[choice - 1]
376
- console.print(f"\n[green]✓ Selected: {selected_adapter['title']}[/green]")
377
- return selected_adapter["name"]
378
- else:
379
- console.print(f"[red]Please enter a number between 1 and {len(adapters)}[/red]")
380
- except (ValueError, typer.Abort):
381
- console.print("[yellow]Setup cancelled.[/yellow]")
382
- raise typer.Exit(0)
383
-
384
-
385
- @app.command()
386
- def setup(
387
- adapter: Optional[str] = typer.Option(
388
- None,
389
- "--adapter",
390
- "-a",
391
- help="Adapter type to use (interactive prompt if not specified)",
392
- ),
393
- project_path: Optional[str] = typer.Option(
394
- None, "--path", help="Project path (default: current directory)"
395
- ),
396
- global_config: bool = typer.Option(
397
- False,
398
- "--global",
399
- "-g",
400
- help="Save to global config instead of project-specific",
401
- ),
402
- base_path: Optional[str] = typer.Option(
403
- None,
404
- "--base-path",
405
- "-p",
406
- help="Base path for ticket storage (AITrackdown only)",
407
- ),
408
- api_key: Optional[str] = typer.Option(
409
- None, "--api-key", help="API key for Linear or API token for JIRA"
410
- ),
411
- team_id: Optional[str] = typer.Option(
412
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
413
- ),
414
- jira_server: Optional[str] = typer.Option(
415
- None,
416
- "--jira-server",
417
- help="JIRA server URL (e.g., https://company.atlassian.net)",
418
- ),
419
- jira_email: Optional[str] = typer.Option(
420
- None, "--jira-email", help="JIRA user email for authentication"
421
- ),
422
- jira_project: Optional[str] = typer.Option(
423
- None, "--jira-project", help="Default JIRA project key"
424
- ),
425
- github_owner: Optional[str] = typer.Option(
426
- None, "--github-owner", help="GitHub repository owner"
427
- ),
428
- github_repo: Optional[str] = typer.Option(
429
- None, "--github-repo", help="GitHub repository name"
430
- ),
431
- github_token: Optional[str] = typer.Option(
432
- None, "--github-token", help="GitHub Personal Access Token"
433
- ),
434
- ) -> None:
435
- """Interactive setup wizard for MCP Ticketer (alias for init).
436
-
437
- This command provides a user-friendly setup experience with prompts
438
- to guide you through configuring MCP Ticketer for your preferred
439
- ticket management system. It's identical to 'init' and 'install'.
440
-
441
- Examples:
442
- # Run interactive setup
443
- mcp-ticketer setup
444
-
445
- # Setup with specific adapter
446
- mcp-ticketer setup --adapter linear
447
-
448
- # Setup for different project
449
- mcp-ticketer setup --path /path/to/project
450
- """
451
- # Call init with all parameters
452
- init(
453
- adapter=adapter,
454
- project_path=project_path,
455
- global_config=global_config,
456
- base_path=base_path,
457
- api_key=api_key,
458
- team_id=team_id,
459
- jira_server=jira_server,
460
- jira_email=jira_email,
461
- jira_project=jira_project,
462
- github_owner=github_owner,
463
- github_repo=github_repo,
464
- github_token=github_token,
465
- )
466
-
467
-
468
- @app.command()
469
- def init(
470
- adapter: Optional[str] = typer.Option(
471
- None,
472
- "--adapter",
473
- "-a",
474
- help="Adapter type to use (interactive prompt if not specified)",
475
- ),
476
- project_path: Optional[str] = typer.Option(
477
- None, "--path", help="Project path (default: current directory)"
478
- ),
479
- global_config: bool = typer.Option(
480
- False,
481
- "--global",
482
- "-g",
483
- help="Save to global config instead of project-specific",
484
- ),
485
- base_path: Optional[str] = typer.Option(
486
- None,
487
- "--base-path",
488
- "-p",
489
- help="Base path for ticket storage (AITrackdown only)",
490
- ),
491
- api_key: Optional[str] = typer.Option(
492
- None, "--api-key", help="API key for Linear or API token for JIRA"
493
- ),
494
- team_id: Optional[str] = typer.Option(
495
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
496
- ),
497
- jira_server: Optional[str] = typer.Option(
498
- None,
499
- "--jira-server",
500
- help="JIRA server URL (e.g., https://company.atlassian.net)",
501
- ),
502
- jira_email: Optional[str] = typer.Option(
503
- None, "--jira-email", help="JIRA user email for authentication"
504
- ),
505
- jira_project: Optional[str] = typer.Option(
506
- None, "--jira-project", help="Default JIRA project key"
507
- ),
508
- github_owner: Optional[str] = typer.Option(
509
- None, "--github-owner", help="GitHub repository owner"
510
- ),
511
- github_repo: Optional[str] = typer.Option(
512
- None, "--github-repo", help="GitHub repository name"
513
- ),
514
- github_token: Optional[str] = typer.Option(
515
- None, "--github-token", help="GitHub Personal Access Token"
516
- ),
517
- ) -> None:
518
- """Initialize mcp-ticketer for the current project.
519
-
520
- This command sets up MCP Ticketer configuration with interactive prompts
521
- to guide you through the process. It auto-detects adapter configuration
522
- from .env files or prompts for interactive setup if no configuration is found.
523
-
524
- Creates .mcp-ticketer/config.json in the current directory with
525
- auto-detected or specified adapter configuration.
526
-
527
- Note: 'setup' and 'install' are synonyms for this command.
528
-
529
- Examples:
530
- # Interactive setup (same as 'setup' and 'install')
531
- mcp-ticketer init
532
-
533
- # Force specific adapter
534
- mcp-ticketer init --adapter linear
535
-
536
- # Initialize for different project
537
- mcp-ticketer init --path /path/to/project
538
-
539
- # Save globally (not recommended)
540
- mcp-ticketer init --global
541
-
542
- """
543
- from pathlib import Path
544
-
545
- from ..core.env_discovery import discover_config
546
- from ..core.project_config import ConfigResolver
547
-
548
- # Determine project path
549
- proj_path = Path(project_path) if project_path else Path.cwd()
550
-
551
- # Check if already initialized (unless using --global)
552
- if not global_config:
553
- config_path = proj_path / ".mcp-ticketer" / "config.json"
554
-
555
- if config_path.exists():
556
- if not typer.confirm(
557
- f"Configuration already exists at {config_path}. Overwrite?",
558
- default=False,
559
- ):
560
- console.print("[yellow]Initialization cancelled.[/yellow]")
561
- raise typer.Exit(0)
562
-
563
- # 1. Try auto-discovery if no adapter specified
564
- discovered = None
565
- adapter_type = adapter
566
-
567
- if not adapter_type:
568
- console.print(
569
- "[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
570
- )
571
- discovered = discover_config(proj_path)
572
-
573
- if discovered and discovered.adapters:
574
- primary = discovered.get_primary_adapter()
575
- if primary:
576
- adapter_type = primary.adapter_type
577
- console.print(
578
- f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
579
- )
580
-
581
- # Show what was discovered
582
- console.print(
583
- f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
584
- )
585
- console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
586
-
587
- # Ask user to confirm auto-detected adapter
588
- if not typer.confirm(
589
- f"Use detected {adapter_type} adapter?",
590
- default=True,
591
- ):
592
- adapter_type = None # Will trigger interactive selection
593
- else:
594
- adapter_type = None # Will trigger interactive selection
595
- else:
596
- adapter_type = None # Will trigger interactive selection
597
-
598
- # If no adapter determined, show interactive selection
599
- if not adapter_type:
600
- adapter_type = _prompt_for_adapter_selection(console)
601
-
602
- # 2. Create configuration based on adapter type
603
- config = {"default_adapter": adapter_type, "adapters": {}}
604
-
605
- # 3. If discovered and matches adapter_type, use discovered config
606
- if discovered and adapter_type != "aitrackdown":
607
- discovered_adapter = discovered.get_adapter_by_type(adapter_type)
608
- if discovered_adapter:
609
- adapter_config = discovered_adapter.config.copy()
610
- # Ensure the config has the correct 'type' field
611
- adapter_config["type"] = adapter_type
612
- # Remove 'adapter' field if present (legacy)
613
- adapter_config.pop("adapter", None)
614
- config["adapters"][adapter_type] = adapter_config
615
-
616
- # 4. Handle manual configuration for specific adapters
617
- if adapter_type == "aitrackdown":
618
- config["adapters"]["aitrackdown"] = {
619
- "type": "aitrackdown",
620
- "base_path": base_path or ".aitrackdown"
621
- }
622
-
623
- elif adapter_type == "linear":
624
- # If not auto-discovered, build from CLI params or prompt
625
- if adapter_type not in config["adapters"]:
626
- linear_config = {}
627
-
628
- # API Key
629
- linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
630
- if not linear_api_key and not discovered:
631
- console.print("\n[bold]Linear Configuration[/bold]")
632
- console.print("You need a Linear API key to connect to Linear.")
633
- console.print("[dim]Get your API key at: https://linear.app/settings/api[/dim]\n")
634
-
635
- linear_api_key = typer.prompt(
636
- "Enter your Linear API key",
637
- hide_input=True
638
- )
639
-
640
- if linear_api_key:
641
- linear_config["api_key"] = linear_api_key
642
-
643
- # Team ID
644
- linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
645
- if not linear_team_id and not discovered:
646
- console.print("\nYou need your Linear team ID.")
647
- console.print("[dim]Find it in Linear settings or team URL[/dim]\n")
648
-
649
- linear_team_id = typer.prompt("Enter your Linear team ID")
650
-
651
- if linear_team_id:
652
- linear_config["team_id"] = linear_team_id
653
-
654
- if not linear_config.get("api_key") or not linear_config.get("team_id"):
655
- console.print("[red]Error:[/red] Linear requires both API key and team ID")
656
- console.print("Run 'mcp-ticketer init --adapter linear' with proper credentials")
657
- raise typer.Exit(1)
658
-
659
- linear_config["type"] = "linear"
660
- config["adapters"]["linear"] = linear_config
661
-
662
- elif adapter_type == "jira":
663
- # If not auto-discovered, build from CLI params or prompt
664
- if adapter_type not in config["adapters"]:
665
- server = jira_server or os.getenv("JIRA_SERVER")
666
- email = jira_email or os.getenv("JIRA_EMAIL")
667
- token = api_key or os.getenv("JIRA_API_TOKEN")
668
- project = jira_project or os.getenv("JIRA_PROJECT_KEY")
669
-
670
- # Interactive prompts for missing values
671
- if not server and not discovered:
672
- console.print("\n[bold]JIRA Configuration[/bold]")
673
- console.print("Enter your JIRA server details.\n")
674
-
675
- server = typer.prompt(
676
- "JIRA server URL (e.g., https://company.atlassian.net)"
677
- )
678
-
679
- if not email and not discovered:
680
- email = typer.prompt("Your JIRA email address")
681
-
682
- if not token and not discovered:
683
- console.print("\nYou need a JIRA API token.")
684
- console.print("[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n")
685
-
686
- token = typer.prompt(
687
- "Enter your JIRA API token",
688
- hide_input=True
689
- )
690
-
691
- if not project and not discovered:
692
- project = typer.prompt(
693
- "Default JIRA project key (optional, press Enter to skip)",
694
- default="",
695
- show_default=False
696
- )
697
-
698
- # Validate required fields
699
- if not server:
700
- console.print("[red]Error:[/red] JIRA server URL is required")
701
- raise typer.Exit(1)
702
-
703
- if not email:
704
- console.print("[red]Error:[/red] JIRA email is required")
705
- raise typer.Exit(1)
706
-
707
- if not token:
708
- console.print("[red]Error:[/red] JIRA API token is required")
709
- raise typer.Exit(1)
710
-
711
- jira_config = {
712
- "server": server,
713
- "email": email,
714
- "api_token": token,
715
- "type": "jira"
716
- }
717
-
718
- if project:
719
- jira_config["project_key"] = project
720
-
721
- config["adapters"]["jira"] = jira_config
722
-
723
- elif adapter_type == "github":
724
- # If not auto-discovered, build from CLI params or prompt
725
- if adapter_type not in config["adapters"]:
726
- owner = github_owner or os.getenv("GITHUB_OWNER")
727
- repo = github_repo or os.getenv("GITHUB_REPO")
728
- token = github_token or os.getenv("GITHUB_TOKEN")
729
-
730
- # Interactive prompts for missing values
731
- if not owner and not discovered:
732
- console.print("\n[bold]GitHub Configuration[/bold]")
733
- console.print("Enter your GitHub repository details.\n")
734
-
735
- owner = typer.prompt("GitHub repository owner (username or organization)")
736
-
737
- if not repo and not discovered:
738
- repo = typer.prompt("GitHub repository name")
739
-
740
- if not token and not discovered:
741
- console.print("\nYou need a GitHub Personal Access Token.")
742
- console.print("[dim]Create one at: https://github.com/settings/tokens/new[/dim]")
743
- console.print("[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n")
744
-
745
- token = typer.prompt(
746
- "Enter your GitHub Personal Access Token",
747
- hide_input=True
748
- )
749
-
750
- # Validate required fields
751
- if not owner:
752
- console.print("[red]Error:[/red] GitHub repository owner is required")
753
- raise typer.Exit(1)
754
-
755
- if not repo:
756
- console.print("[red]Error:[/red] GitHub repository name is required")
757
- raise typer.Exit(1)
758
-
759
- if not token:
760
- console.print("[red]Error:[/red] GitHub Personal Access Token is required")
761
- raise typer.Exit(1)
762
-
763
- config["adapters"]["github"] = {
764
- "owner": owner,
765
- "repo": repo,
766
- "token": token,
767
- "type": "github"
768
- }
769
-
770
- # 5. Save to appropriate location
771
- if global_config:
772
- # Save to ~/.mcp-ticketer/config.json
773
- resolver = ConfigResolver(project_path=proj_path)
774
- config_file_path = resolver.GLOBAL_CONFIG_PATH
775
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
776
-
777
- with open(config_file_path, "w") as f:
778
- json.dump(config, f, indent=2)
779
-
780
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
781
- console.print(f"[dim]Global configuration saved to {config_file_path}[/dim]")
782
- else:
783
- # Save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
784
- config_file_path = proj_path / ".mcp-ticketer" / "config.json"
785
- config_file_path.parent.mkdir(parents=True, exist_ok=True)
786
-
787
- with open(config_file_path, "w") as f:
788
- json.dump(config, f, indent=2)
789
-
790
- console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
791
- console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
792
-
793
- # Add .mcp-ticketer to .gitignore if not already there
794
- gitignore_path = proj_path / ".gitignore"
795
- if gitignore_path.exists():
796
- gitignore_content = gitignore_path.read_text()
797
- if ".mcp-ticketer" not in gitignore_content:
798
- with open(gitignore_path, "a") as f:
799
- f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
800
- console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
801
- else:
802
- # Create .gitignore if it doesn't exist
803
- with open(gitignore_path, "w") as f:
804
- f.write("# MCP Ticketer\n.mcp-ticketer/\n")
805
- console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
806
-
807
- # Show next steps
808
- _show_next_steps(console, adapter_type, config_file_path)
809
-
810
-
811
- def _show_next_steps(console: Console, adapter_type: str, config_file_path: Path) -> None:
812
- """Show helpful next steps after initialization.
813
-
814
- Args:
815
- console: Rich console for output
816
- adapter_type: Type of adapter that was configured
817
- config_file_path: Path to the configuration file
818
- """
819
- console.print("\n[bold green]🎉 Setup Complete![/bold green]")
820
- console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
821
-
822
- console.print("[bold]Next Steps:[/bold]")
823
- console.print("1. [cyan]Test your configuration:[/cyan]")
824
- console.print(" mcp-ticketer diagnose")
825
- console.print("\n2. [cyan]Create a test ticket:[/cyan]")
826
- console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
827
-
828
- if adapter_type != "aitrackdown":
829
- console.print(f"\n3. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]")
830
-
831
- if adapter_type == "linear":
832
- console.print(" Check your Linear workspace for the new ticket")
833
- elif adapter_type == "github":
834
- console.print(" Check your GitHub repository's Issues tab")
835
- elif adapter_type == "jira":
836
- console.print(" Check your JIRA project for the new ticket")
837
- else:
838
- console.print("\n3. [cyan]Check local ticket storage:[/cyan]")
839
- console.print(" ls .aitrackdown/")
840
-
841
- console.print("\n4. [cyan]Configure MCP clients (optional):[/cyan]")
842
- console.print(" mcp-ticketer mcp claude # For Claude Code")
843
- console.print(" mcp-ticketer mcp auggie # For Auggie")
844
- console.print(" mcp-ticketer mcp gemini # For Gemini CLI")
845
-
846
- console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
847
- console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
848
-
849
-
850
- @app.command()
851
- def install(
852
- adapter: Optional[str] = typer.Option(
853
- None,
854
- "--adapter",
855
- "-a",
856
- help="Adapter type to use (auto-detected from .env if not specified)",
857
- ),
858
- project_path: Optional[str] = typer.Option(
859
- None, "--path", help="Project path (default: current directory)"
860
- ),
861
- global_config: bool = typer.Option(
862
- False,
863
- "--global",
864
- "-g",
865
- help="Save to global config instead of project-specific",
866
- ),
867
- base_path: Optional[str] = typer.Option(
868
- None,
869
- "--base-path",
870
- "-p",
871
- help="Base path for ticket storage (AITrackdown only)",
872
- ),
873
- api_key: Optional[str] = typer.Option(
874
- None, "--api-key", help="API key for Linear or API token for JIRA"
875
- ),
876
- team_id: Optional[str] = typer.Option(
877
- None, "--team-id", help="Linear team ID (required for Linear adapter)"
878
- ),
879
- jira_server: Optional[str] = typer.Option(
880
- None,
881
- "--jira-server",
882
- help="JIRA server URL (e.g., https://company.atlassian.net)",
883
- ),
884
- jira_email: Optional[str] = typer.Option(
885
- None, "--jira-email", help="JIRA user email for authentication"
886
- ),
887
- jira_project: Optional[str] = typer.Option(
888
- None, "--jira-project", help="Default JIRA project key"
889
- ),
890
- github_owner: Optional[str] = typer.Option(
891
- None, "--github-owner", help="GitHub repository owner"
892
- ),
893
- github_repo: Optional[str] = typer.Option(
894
- None, "--github-repo", help="GitHub repository name"
895
- ),
896
- github_token: Optional[str] = typer.Option(
897
- None, "--github-token", help="GitHub Personal Access Token"
898
- ),
899
- ) -> None:
900
- """Initialize mcp-ticketer for the current project (alias for init).
901
-
902
- This command is synonymous with 'init' and 'setup' - all three provide
903
- identical functionality with interactive prompts to guide you through
904
- configuring MCP Ticketer for your preferred ticket management system.
905
-
906
- Examples:
907
- # Interactive setup (same as 'init' and 'setup')
908
- mcp-ticketer install
909
-
910
- # Force specific adapter
911
- mcp-ticketer install --adapter linear
912
-
913
- # Initialize for different project
914
- mcp-ticketer install --path /path/to/project
915
-
916
- # Save globally (not recommended)
917
- mcp-ticketer install --global
918
-
919
- """
920
- # Call init with all parameters
921
- init(
922
- adapter=adapter,
923
- project_path=project_path,
924
- global_config=global_config,
925
- base_path=base_path,
926
- api_key=api_key,
927
- team_id=team_id,
928
- jira_server=jira_server,
929
- jira_email=jira_email,
930
- jira_project=jira_project,
931
- github_owner=github_owner,
932
- github_repo=github_repo,
933
- github_token=github_token,
934
- )
935
-
936
-
937
334
  @app.command("set")
938
335
  def set_config(
939
- adapter: Optional[AdapterType] = typer.Option(
336
+ adapter: AdapterType | None = typer.Option(
940
337
  None, "--adapter", "-a", help="Set default adapter"
941
338
  ),
942
- team_key: Optional[str] = typer.Option(
339
+ team_key: str | None = typer.Option(
943
340
  None, "--team-key", help="Linear team key (e.g., BTA)"
944
341
  ),
945
- team_id: Optional[str] = typer.Option(None, "--team-id", help="Linear team ID"),
946
- owner: Optional[str] = typer.Option(
947
- None, "--owner", help="GitHub repository owner"
948
- ),
949
- repo: Optional[str] = typer.Option(None, "--repo", help="GitHub repository name"),
950
- server: Optional[str] = typer.Option(None, "--server", help="JIRA server URL"),
951
- project: Optional[str] = typer.Option(None, "--project", help="JIRA project key"),
952
- 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(
953
348
  None, "--base-path", help="AITrackdown base path"
954
349
  ),
955
350
  ) -> None:
@@ -1039,16 +434,12 @@ def set_config(
1039
434
  @app.command("configure")
1040
435
  def configure_command(
1041
436
  show: bool = typer.Option(False, "--show", help="Show current configuration"),
1042
- adapter: Optional[str] = typer.Option(
437
+ adapter: str | None = typer.Option(
1043
438
  None, "--adapter", help="Set default adapter type"
1044
439
  ),
1045
- api_key: Optional[str] = typer.Option(None, "--api-key", help="Set API key/token"),
1046
- project_id: Optional[str] = typer.Option(
1047
- None, "--project-id", help="Set project ID"
1048
- ),
1049
- team_id: Optional[str] = typer.Option(
1050
- None, "--team-id", help="Set team ID (Linear)"
1051
- ),
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)"),
1052
443
  global_scope: bool = typer.Option(
1053
444
  False,
1054
445
  "--global",
@@ -1082,6 +473,26 @@ def configure_command(
1082
473
  configure_wizard()
1083
474
 
1084
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
+
1085
496
  @app.command("migrate-config")
1086
497
  def migrate_config(
1087
498
  dry_run: bool = typer.Option(
@@ -1099,630 +510,74 @@ def migrate_config(
1099
510
  migrate_config_command(dry_run=dry_run)
1100
511
 
1101
512
 
1102
- @app.command("status")
1103
- def status_command():
1104
- """Show queue and worker status."""
1105
- queue = Queue()
1106
- manager = WorkerManager()
1107
-
1108
- # Get queue stats
1109
- stats = queue.get_stats()
1110
- pending = stats.get(QueueStatus.PENDING.value, 0)
1111
-
1112
- # Show queue status
1113
- console.print("[bold]Queue Status:[/bold]")
1114
- console.print(f" Pending: {pending}")
1115
- console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
1116
- console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
1117
- console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
1118
-
1119
- # Show worker status
1120
- worker_status = manager.get_status()
1121
- if worker_status["running"]:
1122
- console.print(
1123
- f"\n[green]● Worker is running[/green] (PID: {worker_status.get('pid')})"
1124
- )
1125
- else:
1126
- console.print("\n[red]○ Worker is not running[/red]")
1127
- if pending > 0:
1128
- console.print(
1129
- "[yellow]Note: There are pending items. Start worker with 'mcp-ticketer worker start'[/yellow]"
1130
- )
1131
-
1132
-
1133
- @app.command()
1134
- def health(
1135
- auto_repair: bool = typer.Option(False, "--auto-repair", help="Attempt automatic repair of issues"),
1136
- verbose: bool = typer.Option(False, "--verbose", "-v", help="Show detailed health information")
1137
- ) -> None:
1138
- """Check queue system health and detect issues immediately."""
1139
-
1140
- health_monitor = QueueHealthMonitor()
1141
- health = health_monitor.check_health()
1142
-
1143
- # Display overall status
1144
- status_color = {
1145
- HealthStatus.HEALTHY: "green",
1146
- HealthStatus.WARNING: "yellow",
1147
- HealthStatus.CRITICAL: "red",
1148
- HealthStatus.FAILED: "red"
1149
- }
1150
-
1151
- status_icon = {
1152
- HealthStatus.HEALTHY: "✓",
1153
- HealthStatus.WARNING: "⚠️",
1154
- HealthStatus.CRITICAL: "🚨",
1155
- HealthStatus.FAILED: "❌"
1156
- }
1157
-
1158
- color = status_color.get(health["status"], "white")
1159
- icon = status_icon.get(health["status"], "?")
1160
-
1161
- console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
1162
- console.print(f"Last checked: {health['timestamp']}")
1163
-
1164
- # Display alerts
1165
- if health["alerts"]:
1166
- console.print("\n[bold]Issues Found:[/bold]")
1167
- for alert in health["alerts"]:
1168
- alert_color = status_color.get(alert["level"], "white")
1169
- console.print(f"[{alert_color}] • {alert['message']}[/{alert_color}]")
1170
-
1171
- if verbose and alert.get("details"):
1172
- for key, value in alert["details"].items():
1173
- console.print(f" {key}: {value}")
1174
- else:
1175
- console.print("\n[green]✓ No issues detected[/green]")
1176
-
1177
- # Auto-repair if requested
1178
- if auto_repair and health["status"] in [HealthStatus.CRITICAL, HealthStatus.WARNING]:
1179
- console.print("\n[yellow]Attempting automatic repair...[/yellow]")
1180
- repair_result = health_monitor.auto_repair()
1181
-
1182
- if repair_result["actions_taken"]:
1183
- console.print("[green]Repair actions taken:[/green]")
1184
- for action in repair_result["actions_taken"]:
1185
- console.print(f"[green] ✓ {action}[/green]")
1186
-
1187
- # Re-check health
1188
- console.print("\n[yellow]Re-checking health after repair...[/yellow]")
1189
- new_health = health_monitor.check_health()
1190
- new_color = status_color.get(new_health["status"], "white")
1191
- new_icon = status_icon.get(new_health["status"], "?")
1192
- console.print(f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]")
1193
- else:
1194
- console.print("[yellow]No repair actions available[/yellow]")
1195
-
1196
- # Exit with appropriate code
1197
- if health["status"] == HealthStatus.CRITICAL:
1198
- raise typer.Exit(1)
1199
- elif health["status"] == HealthStatus.WARNING:
1200
- raise typer.Exit(2)
1201
-
1202
-
1203
- @app.command()
1204
- def create(
1205
- title: str = typer.Argument(..., help="Ticket title"),
1206
- description: Optional[str] = typer.Option(
1207
- None, "--description", "-d", help="Ticket description"
1208
- ),
1209
- priority: Priority = typer.Option(
1210
- Priority.MEDIUM, "--priority", "-p", help="Priority level"
1211
- ),
1212
- tags: Optional[list[str]] = typer.Option(
1213
- None, "--tag", "-t", help="Tags (can be specified multiple times)"
1214
- ),
1215
- assignee: Optional[str] = typer.Option(
1216
- None, "--assignee", "-a", help="Assignee username"
1217
- ),
1218
- adapter: Optional[AdapterType] = typer.Option(
1219
- None, "--adapter", help="Override default adapter"
1220
- ),
1221
- ) -> None:
1222
- """Create a new ticket with comprehensive health checks."""
1223
-
1224
- # IMMEDIATE HEALTH CHECK - Critical for reliability
1225
- health_monitor = QueueHealthMonitor()
1226
- health = health_monitor.check_health()
1227
-
1228
- # Display health status
1229
- if health["status"] == HealthStatus.CRITICAL:
1230
- console.print("[red]🚨 CRITICAL: Queue system has serious issues![/red]")
1231
- for alert in health["alerts"]:
1232
- if alert["level"] == "critical":
1233
- console.print(f"[red] • {alert['message']}[/red]")
1234
-
1235
- # Attempt auto-repair
1236
- console.print("[yellow]Attempting automatic repair...[/yellow]")
1237
- repair_result = health_monitor.auto_repair()
1238
-
1239
- if repair_result["actions_taken"]:
1240
- for action in repair_result["actions_taken"]:
1241
- console.print(f"[yellow] ✓ {action}[/yellow]")
1242
-
1243
- # Re-check health after repair
1244
- health = health_monitor.check_health()
1245
- if health["status"] == HealthStatus.CRITICAL:
1246
- console.print("[red]❌ Auto-repair failed. Manual intervention required.[/red]")
1247
- console.print("[red]Cannot safely create ticket. Please check system status.[/red]")
1248
- raise typer.Exit(1)
1249
- else:
1250
- console.print("[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]")
1251
- else:
1252
- console.print("[red]❌ No repair actions available. Manual intervention required.[/red]")
1253
- raise typer.Exit(1)
1254
-
1255
- elif health["status"] == HealthStatus.WARNING:
1256
- console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
1257
- for alert in health["alerts"]:
1258
- if alert["level"] == "warning":
1259
- console.print(f"[yellow] • {alert['message']}[/yellow]")
1260
- console.print("[yellow]Proceeding with ticket creation...[/yellow]")
1261
-
1262
- # Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
1263
- if adapter:
1264
- # Priority 1: Command-line argument - save to config for future use
1265
- adapter_name = adapter.value
1266
- _save_adapter_to_config(adapter_name)
1267
- else:
1268
- # Priority 2: Check existing config
1269
- config = load_config()
1270
- adapter_name = config.get("default_adapter")
1271
-
1272
- if not adapter_name or adapter_name == "aitrackdown":
1273
- # Priority 3: Check .env files and save if found
1274
- env_adapter = _discover_from_env_files()
1275
- if env_adapter:
1276
- adapter_name = env_adapter
1277
- _save_adapter_to_config(adapter_name)
1278
- else:
1279
- # Priority 4: Default
1280
- adapter_name = "aitrackdown"
1281
-
1282
- # Create task data
1283
- # Import Priority for type checking
1284
- from ..core.models import Priority as PriorityEnum
1285
-
1286
- task_data = {
1287
- "title": title,
1288
- "description": description,
1289
- "priority": priority.value if isinstance(priority, PriorityEnum) else priority,
1290
- "tags": tags or [],
1291
- "assignee": assignee,
1292
- }
1293
-
1294
- # WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
1295
- if adapter_name == "linear":
1296
- console.print("[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)")
1297
- try:
1298
- # Load configuration and create adapter directly
1299
- config = load_config()
1300
- adapter_config = config.get("adapters", {}).get(adapter_name, {})
1301
-
1302
- # Import and create adapter
1303
- from ..core.registry import AdapterRegistry
1304
- adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
1305
-
1306
- # Create task directly
1307
- from ..core.models import Task, Priority
1308
- task = Task(
1309
- title=task_data["title"],
1310
- description=task_data.get("description"),
1311
- priority=Priority(task_data["priority"]) if task_data.get("priority") else Priority.MEDIUM,
1312
- tags=task_data.get("tags", []),
1313
- assignee=task_data.get("assignee")
1314
- )
1315
-
1316
- # Create ticket synchronously
1317
- import asyncio
1318
- result = asyncio.run(adapter.create(task))
1319
-
1320
- console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
1321
- console.print(f" Title: {result.title}")
1322
- console.print(f" Priority: {result.priority}")
1323
- console.print(f" State: {result.state}")
1324
- # Get URL from metadata if available
1325
- if result.metadata and 'linear' in result.metadata and 'url' in result.metadata['linear']:
1326
- console.print(f" URL: {result.metadata['linear']['url']}")
1327
-
1328
- return result.id
1329
-
1330
- except Exception as e:
1331
- console.print(f"[red]❌[/red] Failed to create ticket: {e}")
1332
- raise
1333
-
1334
- # Use queue for other adapters
1335
- queue = Queue()
1336
- queue_id = queue.add(
1337
- ticket_data=task_data,
1338
- adapter=adapter_name,
1339
- operation="create",
1340
- project_dir=str(Path.cwd()) # Explicitly pass current project directory
1341
- )
1342
-
1343
- # Register in ticket registry for tracking
1344
- registry = TicketRegistry()
1345
- registry.register_ticket_operation(queue_id, adapter_name, "create", title, task_data)
1346
-
1347
- console.print(f"[green]✓[/green] Queued ticket creation: {queue_id}")
1348
- console.print(f" Title: {title}")
1349
- console.print(f" Priority: {priority}")
1350
- console.print(f" Adapter: {adapter_name}")
1351
- console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
1352
-
1353
- # Start worker if needed with immediate feedback
1354
- manager = WorkerManager()
1355
- worker_started = manager.start_if_needed()
1356
-
1357
- if worker_started:
1358
- console.print("[dim]Worker started to process request[/dim]")
1359
-
1360
- # Give immediate feedback on processing
1361
- import time
1362
- time.sleep(1) # Brief pause to let worker start
1363
-
1364
- # Check if item is being processed
1365
- item = queue.get_item(queue_id)
1366
- if item and item.status == QueueStatus.PROCESSING:
1367
- console.print("[green]✓ Item is being processed by worker[/green]")
1368
- elif item and item.status == QueueStatus.PENDING:
1369
- console.print("[yellow]⏳ Item is queued for processing[/yellow]")
1370
- else:
1371
- console.print("[red]⚠️ Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]")
1372
- else:
1373
- # Worker didn't start - this is a problem
1374
- pending_count = queue.get_pending_count()
1375
- if pending_count > 1: # More than just this item
1376
- console.print(f"[red]❌ Worker failed to start with {pending_count} pending items![/red]")
1377
- console.print("[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]")
1378
- else:
1379
- console.print("[yellow]Worker not started (no other pending items)[/yellow]")
1380
-
1381
-
1382
- @app.command("list")
1383
- def list_tickets(
1384
- state: Optional[TicketState] = typer.Option(
1385
- None, "--state", "-s", help="Filter by state"
1386
- ),
1387
- priority: Optional[Priority] = typer.Option(
1388
- None, "--priority", "-p", help="Filter by priority"
1389
- ),
1390
- limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
1391
- adapter: Optional[AdapterType] = typer.Option(
1392
- None, "--adapter", help="Override default adapter"
1393
- ),
1394
- ) -> None:
1395
- """List tickets with optional filters."""
1396
-
1397
- async def _list():
1398
- adapter_instance = get_adapter(
1399
- override_adapter=adapter.value if adapter else None
1400
- )
1401
- filters = {}
1402
- if state:
1403
- filters["state"] = state
1404
- if priority:
1405
- filters["priority"] = priority
1406
- return await adapter_instance.list(limit=limit, filters=filters)
1407
-
1408
- tickets = asyncio.run(_list())
1409
-
1410
- if not tickets:
1411
- console.print("[yellow]No tickets found[/yellow]")
1412
- return
1413
-
1414
- # Create table
1415
- table = Table(title="Tickets")
1416
- table.add_column("ID", style="cyan", no_wrap=True)
1417
- table.add_column("Title", style="white")
1418
- table.add_column("State", style="green")
1419
- table.add_column("Priority", style="yellow")
1420
- table.add_column("Assignee", style="blue")
1421
-
1422
- for ticket in tickets:
1423
- # Handle assignee field - Epic doesn't have assignee, Task does
1424
- assignee = getattr(ticket, 'assignee', None) or "-"
1425
-
1426
- table.add_row(
1427
- ticket.id or "N/A",
1428
- ticket.title,
1429
- ticket.state,
1430
- ticket.priority,
1431
- assignee,
1432
- )
513
+ # Add ticket command group to main app
514
+ app.add_typer(ticket_app, name="ticket")
1433
515
 
1434
- console.print(table)
516
+ # Add platform command group to main app
517
+ app.add_typer(platform_app, name="platform")
1435
518
 
519
+ # Add queue command to main app
520
+ app.add_typer(queue_app, name="queue")
1436
521
 
1437
- @app.command()
1438
- def show(
1439
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1440
- comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
1441
- adapter: Optional[AdapterType] = typer.Option(
1442
- None, "--adapter", help="Override default adapter"
1443
- ),
1444
- ) -> None:
1445
- """Show detailed ticket information."""
522
+ # Add discover command to main app
523
+ app.add_typer(discover_app, name="discover")
1446
524
 
1447
- async def _show():
1448
- adapter_instance = get_adapter(
1449
- override_adapter=adapter.value if adapter else None
1450
- )
1451
- ticket = await adapter_instance.read(ticket_id)
1452
- ticket_comments = None
1453
- if comments and ticket:
1454
- ticket_comments = await adapter_instance.get_comments(ticket_id)
1455
- return ticket, ticket_comments
1456
-
1457
- ticket, ticket_comments = asyncio.run(_show())
1458
-
1459
- if not ticket:
1460
- console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
1461
- raise typer.Exit(1)
1462
-
1463
- # Display ticket details
1464
- console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
1465
- console.print(f"Title: {ticket.title}")
1466
- console.print(f"State: [green]{ticket.state}[/green]")
1467
- console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
1468
-
1469
- if ticket.description:
1470
- console.print("\n[dim]Description:[/dim]")
1471
- console.print(ticket.description)
1472
-
1473
- if ticket.tags:
1474
- console.print(f"\nTags: {', '.join(ticket.tags)}")
1475
-
1476
- if ticket.assignee:
1477
- console.print(f"Assignee: {ticket.assignee}")
1478
-
1479
- # Display comments if requested
1480
- if ticket_comments:
1481
- console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
1482
- for comment in ticket_comments:
1483
- console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
1484
- console.print(comment.content)
1485
-
1486
-
1487
- @app.command()
1488
- def comment(
1489
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1490
- content: str = typer.Argument(..., help="Comment content"),
1491
- adapter: Optional[AdapterType] = typer.Option(
1492
- None, "--adapter", help="Override default adapter"
1493
- ),
1494
- ) -> None:
1495
- """Add a comment to a ticket."""
525
+ # Add instructions command to main app
526
+ app.add_typer(instruction_app, name="instructions")
1496
527
 
1497
- async def _comment():
1498
- adapter_instance = get_adapter(
1499
- override_adapter=adapter.value if adapter else None
1500
- )
528
+ # Add project-update command group to main app
529
+ app.add_typer(project_update_app, name="project-update")
1501
530
 
1502
- # Create comment
1503
- comment = Comment(
1504
- ticket_id=ticket_id,
1505
- content=content,
1506
- author="cli-user" # Could be made configurable
1507
- )
531
+ # Add setup and init commands to main app
532
+ app.command()(setup)
533
+ app.command()(init)
1508
534
 
1509
- result = await adapter_instance.add_comment(comment)
1510
- return result
535
+ # Add platform installer commands to main app
536
+ app.command()(install)
537
+ app.command()(remove)
538
+ app.command()(uninstall)
1511
539
 
1512
- try:
1513
- result = asyncio.run(_comment())
1514
- console.print(f"[green]✓[/green] Comment added successfully")
1515
- if result.id:
1516
- console.print(f"Comment ID: {result.id}")
1517
- console.print(f"Content: {content}")
1518
- except Exception as e:
1519
- console.print(f"[red]✗[/red] Failed to add comment: {e}")
1520
- 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)
1521
544
 
1522
545
 
1523
- @app.command()
1524
- def update(
1525
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1526
- title: Optional[str] = typer.Option(None, "--title", help="New title"),
1527
- description: Optional[str] = typer.Option(
1528
- None, "--description", "-d", help="New description"
1529
- ),
1530
- priority: Optional[Priority] = typer.Option(
1531
- None, "--priority", "-p", help="New priority"
1532
- ),
1533
- assignee: Optional[str] = typer.Option(
1534
- None, "--assignee", "-a", help="New assignee"
1535
- ),
1536
- adapter: Optional[AdapterType] = typer.Option(
1537
- None, "--adapter", help="Override default adapter"
1538
- ),
1539
- ) -> None:
1540
- """Update ticket fields."""
1541
- updates = {}
1542
- if title:
1543
- updates["title"] = title
1544
- if description:
1545
- updates["description"] = description
1546
- if priority:
1547
- updates["priority"] = (
1548
- priority.value if isinstance(priority, Priority) else priority
1549
- )
1550
- if assignee:
1551
- updates["assignee"] = assignee
1552
-
1553
- if not updates:
1554
- console.print("[yellow]No updates specified[/yellow]")
1555
- raise typer.Exit(1)
1556
-
1557
- # Get the adapter name
1558
- config = load_config()
1559
- adapter_name = (
1560
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1561
- )
1562
-
1563
- # Add ticket_id to updates
1564
- updates["ticket_id"] = ticket_id
1565
-
1566
- # Add to queue with explicit project directory
1567
- queue = Queue()
1568
- queue_id = queue.add(
1569
- ticket_data=updates,
1570
- adapter=adapter_name,
1571
- operation="update",
1572
- project_dir=str(Path.cwd()) # Explicitly pass current project directory
1573
- )
1574
-
1575
- console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
1576
- for key, value in updates.items():
1577
- if key != "ticket_id":
1578
- console.print(f" {key}: {value}")
1579
- console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
1580
-
1581
- # Start worker if needed
1582
- manager = WorkerManager()
1583
- if manager.start_if_needed():
1584
- console.print("[dim]Worker started to process request[/dim]")
1585
-
1586
-
1587
- @app.command()
1588
- def transition(
1589
- ticket_id: str = typer.Argument(..., help="Ticket ID"),
1590
- state_positional: Optional[TicketState] = typer.Argument(
1591
- 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"
1592
551
  ),
1593
- state: Optional[TicketState] = typer.Option(
1594
- None, "--state", "-s", help="Target state (recommended)"
552
+ json_output: bool = typer.Option(
553
+ False, "--json", help="Output report in JSON format"
1595
554
  ),
1596
- adapter: Optional[AdapterType] = typer.Option(
1597
- None, "--adapter", help="Override default adapter"
555
+ simple: bool = typer.Option(
556
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1598
557
  ),
1599
558
  ) -> None:
1600
- """Change ticket state with validation.
1601
-
1602
- Examples:
1603
- # Recommended syntax with flag:
1604
- mcp-ticketer transition BTA-215 --state done
1605
- mcp-ticketer transition BTA-215 -s in_progress
1606
-
1607
- # Legacy positional syntax (still supported):
1608
- mcp-ticketer transition BTA-215 done
1609
-
1610
- """
1611
- # Determine which state to use (prefer flag over positional)
1612
- target_state = state if state is not None else state_positional
1613
-
1614
- if target_state is None:
1615
- console.print("[red]Error: State is required[/red]")
1616
- console.print(
1617
- "Use either:\n"
1618
- " - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
1619
- " - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
1620
- )
1621
- raise typer.Exit(1)
1622
-
1623
- # Get the adapter name
1624
- config = load_config()
1625
- adapter_name = (
1626
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
1627
- )
1628
-
1629
- # Add to queue with explicit project directory
1630
- queue = Queue()
1631
- queue_id = queue.add(
1632
- ticket_data={
1633
- "ticket_id": ticket_id,
1634
- "state": (
1635
- target_state.value if hasattr(target_state, "value") else target_state
1636
- ),
1637
- },
1638
- adapter=adapter_name,
1639
- operation="transition",
1640
- project_dir=str(Path.cwd()) # Explicitly pass current project directory
1641
- )
1642
-
1643
- console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
1644
- console.print(f" Ticket: {ticket_id} → {target_state}")
1645
- console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
1646
-
1647
- # Start worker if needed
1648
- manager = WorkerManager()
1649
- if manager.start_if_needed():
1650
- console.print("[dim]Worker started to process request[/dim]")
1651
-
1652
-
1653
- @app.command()
1654
- def search(
1655
- query: Optional[str] = typer.Argument(None, help="Search query"),
1656
- state: Optional[TicketState] = typer.Option(None, "--state", "-s"),
1657
- priority: Optional[Priority] = typer.Option(None, "--priority", "-p"),
1658
- assignee: Optional[str] = typer.Option(None, "--assignee", "-a"),
1659
- limit: int = typer.Option(10, "--limit", "-l"),
1660
- adapter: Optional[AdapterType] = typer.Option(
1661
- None, "--adapter", help="Override default adapter"
1662
- ),
1663
- ) -> None:
1664
- """Search tickets with advanced query."""
1665
-
1666
- async def _search():
1667
- adapter_instance = get_adapter(
1668
- override_adapter=adapter.value if adapter else None
1669
- )
1670
- search_query = SearchQuery(
1671
- query=query,
1672
- state=state,
1673
- priority=priority,
1674
- assignee=assignee,
1675
- limit=limit,
1676
- )
1677
- return await adapter_instance.search(search_query)
1678
-
1679
- tickets = asyncio.run(_search())
1680
-
1681
- if not tickets:
1682
- console.print("[yellow]No tickets found matching query[/yellow]")
1683
- return
1684
-
1685
- # Display results
1686
- console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
1687
-
1688
- for ticket in tickets:
1689
- console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
1690
- console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
1691
- if ticket.assignee:
1692
- console.print(f" Assignee: {ticket.assignee}")
1693
- console.print()
1694
-
1695
-
1696
- # Add queue command to main app
1697
- app.add_typer(queue_app, name="queue")
1698
-
1699
- # Add discover command to main app
1700
- app.add_typer(discover_app, name="discover")
1701
-
1702
- # Add diagnostics command
1703
- @app.command()
1704
- def diagnose(
1705
- output_file: Optional[str] = typer.Option(None, "--output", "-o", help="Save full report to file"),
1706
- json_output: bool = typer.Option(False, "--json", help="Output report in JSON format"),
1707
- simple: bool = typer.Option(False, "--simple", help="Use simple diagnostics (no heavy dependencies)"),
1708
- ) -> None:
1709
- """Run comprehensive system diagnostics and health check."""
559
+ """Run comprehensive system diagnostics and health check (alias: diagnose)."""
1710
560
  if simple:
1711
561
  from .simple_health import simple_diagnose
562
+
1712
563
  report = simple_diagnose()
1713
564
  if output_file:
1714
565
  import json
1715
- with open(output_file, 'w') as f:
566
+
567
+ with open(output_file, "w") as f:
1716
568
  json.dump(report, f, indent=2)
1717
569
  console.print(f"\n📄 Report saved to: {output_file}")
1718
570
  if json_output:
1719
571
  import json
572
+
1720
573
  console.print("\n" + json.dumps(report, indent=2))
1721
574
  if report["issues"]:
1722
- raise typer.Exit(1)
575
+ raise typer.Exit(1) from None
1723
576
  else:
1724
577
  try:
1725
- 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
+ )
1726
581
  except typer.Exit:
1727
582
  # typer.Exit is expected - don't fall back to simple diagnostics
1728
583
  raise
@@ -1730,314 +585,55 @@ def diagnose(
1730
585
  console.print(f"⚠️ Full diagnostics failed: {e}")
1731
586
  console.print("🔄 Falling back to simple diagnostics...")
1732
587
  from .simple_health import simple_diagnose
588
+
1733
589
  report = simple_diagnose()
1734
590
  if report["issues"]:
1735
- raise typer.Exit(1)
1736
-
1737
-
1738
- @app.command()
1739
- def health() -> None:
1740
- """Quick health check - shows system status summary."""
1741
- from .simple_health import simple_health_check
1742
-
1743
- result = simple_health_check()
1744
- if result != 0:
1745
- raise typer.Exit(result)
1746
-
1747
- # Create MCP configuration command group
1748
- mcp_app = typer.Typer(
1749
- name="mcp",
1750
- help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
1751
- add_completion=False,
1752
- )
1753
-
1754
-
1755
- @app.command()
1756
- def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
1757
- """Check status of a queued operation."""
1758
- queue = Queue()
1759
- item = queue.get_item(queue_id)
1760
-
1761
- if not item:
1762
- console.print(f"[red]Queue item not found: {queue_id}[/red]")
1763
- raise typer.Exit(1)
1764
-
1765
- # Display status
1766
- console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
1767
- console.print(f"Operation: {item.operation}")
1768
- console.print(f"Adapter: {item.adapter}")
1769
-
1770
- # Status with color
1771
- if item.status == QueueStatus.COMPLETED:
1772
- console.print(f"Status: [green]{item.status}[/green]")
1773
- elif item.status == QueueStatus.FAILED:
1774
- console.print(f"Status: [red]{item.status}[/red]")
1775
- elif item.status == QueueStatus.PROCESSING:
1776
- console.print(f"Status: [yellow]{item.status}[/yellow]")
1777
- else:
1778
- console.print(f"Status: {item.status}")
1779
-
1780
- # Timestamps
1781
- console.print(f"Created: {item.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
1782
- if item.processed_at:
1783
- console.print(f"Processed: {item.processed_at.strftime('%Y-%m-%d %H:%M:%S')}")
1784
-
1785
- # Error or result
1786
- if item.error_message:
1787
- console.print(f"\n[red]Error:[/red] {item.error_message}")
1788
- elif item.result:
1789
- console.print("\n[green]Result:[/green]")
1790
- for key, value in item.result.items():
1791
- console.print(f" {key}: {value}")
1792
-
1793
- if item.retry_count > 0:
1794
- console.print(f"\nRetry Count: {item.retry_count}")
591
+ raise typer.Exit(1) from None
1795
592
 
1796
593
 
1797
-
1798
-
1799
-
1800
- @app.command()
1801
- def serve(
1802
- adapter: Optional[AdapterType] = typer.Option(
1803
- 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"
1804
598
  ),
1805
- base_path: Optional[str] = typer.Option(
1806
- None, "--base-path", help="Base path for AITrackdown adapter"
1807
- ),
1808
- ):
1809
- """Start MCP server for JSON-RPC communication over stdio.
1810
-
1811
- This command is used by Claude Code/Desktop when connecting to the MCP server.
1812
- You typically don't need to run this manually - use 'mcp-ticketer mcp' to configure.
1813
-
1814
- Configuration Resolution:
1815
- - When MCP server starts, it uses the current working directory (cwd)
1816
- - The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
1817
- - Configuration is loaded with this priority:
1818
- 1. Project-specific: .mcp-ticketer/config.json in cwd
1819
- 2. Global: ~/.mcp-ticketer/config.json
1820
- 3. Default: aitrackdown adapter with .aitrackdown base path
1821
- """
1822
- from ..mcp.server import MCPTicketServer
1823
-
1824
- # Load configuration (respects project-specific config in cwd)
1825
- config = load_config()
1826
-
1827
- # Determine adapter type with priority: CLI arg > .env files > config > default
1828
- if adapter:
1829
- # Priority 1: Command line argument
1830
- adapter_type = adapter.value
1831
- # Get base config from config file
1832
- adapters_config = config.get("adapters", {})
1833
- adapter_config = adapters_config.get(adapter_type, {})
1834
- else:
1835
- # Priority 2: .env files
1836
- from ..mcp.server import _load_env_configuration
1837
- env_config = _load_env_configuration()
1838
- if env_config:
1839
- adapter_type = env_config["adapter_type"]
1840
- adapter_config = env_config["adapter_config"]
1841
- else:
1842
- # Priority 3: Configuration file
1843
- adapter_type = config.get("default_adapter", "aitrackdown")
1844
- adapters_config = config.get("adapters", {})
1845
- adapter_config = adapters_config.get(adapter_type, {})
1846
-
1847
- # Override with command line options if provided (highest priority)
1848
- if base_path and adapter_type == "aitrackdown":
1849
- adapter_config["base_path"] = base_path
1850
-
1851
- # Fallback to legacy config format
1852
- if not adapter_config and "config" in config:
1853
- adapter_config = config["config"]
1854
-
1855
- # MCP server uses stdio for JSON-RPC, so we can't print to stdout
1856
- # Only print to stderr to avoid interfering with the protocol
1857
- import sys
1858
-
1859
- if sys.stderr.isatty():
1860
- # Only print if stderr is a terminal (not redirected)
1861
- console.file = sys.stderr
1862
- console.print(f"[green]Starting MCP server[/green] with {adapter_type} adapter")
1863
- console.print(
1864
- "[dim]Server running on stdio. Send JSON-RPC requests via stdin.[/dim]"
1865
- )
1866
-
1867
- # Create and run server
1868
- try:
1869
- server = MCPTicketServer(adapter_type, adapter_config)
1870
- asyncio.run(server.run())
1871
- except KeyboardInterrupt:
1872
- # Also send this to stderr
1873
- if sys.stderr.isatty():
1874
- console.print("\n[yellow]Server stopped by user[/yellow]")
1875
- if "server" in locals():
1876
- asyncio.run(server.stop())
1877
- except Exception as e:
1878
- # Log error to stderr
1879
- sys.stderr.write(f"MCP server error: {e}\n")
1880
- sys.exit(1)
1881
-
1882
-
1883
- @mcp_app.command(name="claude")
1884
- def mcp_claude(
1885
- global_config: bool = typer.Option(
1886
- False,
1887
- "--global",
1888
- "-g",
1889
- help="Configure Claude Desktop instead of project-level",
599
+ json_output: bool = typer.Option(
600
+ False, "--json", help="Output report in JSON format"
1890
601
  ),
1891
- force: bool = typer.Option(
1892
- False, "--force", "-f", help="Overwrite existing configuration"
602
+ simple: bool = typer.Option(
603
+ False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
1893
604
  ),
1894
- ):
1895
- """Configure Claude Code to use mcp-ticketer MCP server.
1896
-
1897
- Reads configuration from .mcp-ticketer/config.json and updates
1898
- Claude Code's MCP settings accordingly.
1899
-
1900
- By default, configures project-level (.mcp/config.json).
1901
- Use --global to configure Claude Desktop instead.
1902
-
1903
- Examples:
1904
- # Configure for current project (default)
1905
- mcp-ticketer mcp claude
1906
-
1907
- # Configure Claude Desktop globally
1908
- mcp-ticketer mcp claude --global
1909
-
1910
- # Force overwrite existing configuration
1911
- mcp-ticketer mcp claude --force
1912
-
1913
- """
1914
- from ..cli.mcp_configure import configure_claude_mcp
1915
-
1916
- try:
1917
- configure_claude_mcp(global_config=global_config, force=force)
1918
- except Exception as e:
1919
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1920
- raise typer.Exit(1)
1921
-
1922
-
1923
- @mcp_app.command(name="gemini")
1924
- def mcp_gemini(
1925
- scope: str = typer.Option(
1926
- "project",
1927
- "--scope",
1928
- "-s",
1929
- help="Configuration scope: 'project' (default) or 'user'",
1930
- ),
1931
- force: bool = typer.Option(
1932
- False, "--force", "-f", help="Overwrite existing configuration"
1933
- ),
1934
- ):
1935
- """Configure Gemini CLI to use mcp-ticketer MCP server.
1936
-
1937
- Reads configuration from .mcp-ticketer/config.json and creates
1938
- Gemini CLI settings file with mcp-ticketer configuration.
1939
-
1940
- By default, configures project-level (.gemini/settings.json).
1941
- Use --scope user to configure user-level (~/.gemini/settings.json).
1942
-
1943
- Examples:
1944
- # Configure for current project (default)
1945
- mcp-ticketer mcp gemini
1946
-
1947
- # Configure at user level
1948
- mcp-ticketer mcp gemini --scope user
1949
-
1950
- # Force overwrite existing configuration
1951
- mcp-ticketer mcp gemini --force
1952
-
1953
- """
1954
- from ..cli.gemini_configure import configure_gemini_mcp
1955
-
1956
- # Validate scope parameter
1957
- if scope not in ["project", "user"]:
1958
- console.print(
1959
- f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
1960
- )
1961
- raise typer.Exit(1)
1962
-
1963
- try:
1964
- configure_gemini_mcp(scope=scope, force=force) # type: ignore
1965
- except Exception as e:
1966
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1967
- raise typer.Exit(1)
1968
-
1969
-
1970
- @mcp_app.command(name="codex")
1971
- def mcp_codex(
1972
- force: bool = typer.Option(
1973
- False, "--force", "-f", help="Overwrite existing configuration"
1974
- ),
1975
- ):
1976
- """Configure Codex CLI to use mcp-ticketer MCP server.
1977
-
1978
- Reads configuration from .mcp-ticketer/config.json and creates
1979
- Codex CLI config.toml with mcp-ticketer configuration.
1980
-
1981
- IMPORTANT: Codex CLI ONLY supports global configuration at ~/.codex/config.toml.
1982
- There is no project-level configuration support. After configuration,
1983
- you must restart Codex CLI for changes to take effect.
1984
-
1985
- Examples:
1986
- # Configure Codex CLI globally
1987
- mcp-ticketer mcp codex
1988
-
1989
- # Force overwrite existing configuration
1990
- mcp-ticketer mcp codex --force
1991
-
1992
- """
1993
- from ..cli.codex_configure import configure_codex_mcp
1994
-
1995
- try:
1996
- configure_codex_mcp(force=force)
1997
- except Exception as e:
1998
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
1999
- raise typer.Exit(1)
2000
-
2001
-
2002
- @mcp_app.command(name="auggie")
2003
- def mcp_auggie(
2004
- force: bool = typer.Option(
2005
- False, "--force", "-f", help="Overwrite existing configuration"
2006
- ),
2007
- ):
2008
- """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)
2009
609
 
2010
- Reads configuration from .mcp-ticketer/config.json and creates
2011
- Auggie CLI settings.json with mcp-ticketer configuration.
2012
610
 
2013
- IMPORTANT: Auggie CLI ONLY supports global configuration at ~/.augment/settings.json.
2014
- There is no project-level configuration support. After configuration,
2015
- 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
2016
615
 
2017
- Examples:
2018
- # Configure Auggie CLI globally
2019
- mcp-ticketer mcp auggie
616
+ result = simple_health_check()
617
+ if result != 0:
618
+ raise typer.Exit(result) from None
2020
619
 
2021
- # Force overwrite existing configuration
2022
- mcp-ticketer mcp auggie --force
2023
620
 
2024
- """
2025
- 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
2026
625
 
2027
- try:
2028
- configure_auggie_mcp(force=force)
2029
- except Exception as e:
2030
- console.print(f"[red]✗ Configuration failed:[/red] {e}")
2031
- raise typer.Exit(1)
626
+ result = simple_health_check()
627
+ if result != 0:
628
+ raise typer.Exit(result) from None
2032
629
 
2033
630
 
2034
631
  # Add command groups to main app (must be after all subcommands are defined)
2035
- app.add_typer(linear_app, name="linear")
2036
632
  app.add_typer(mcp_app, name="mcp")
2037
633
 
2038
634
 
2039
- def main():
2040
- """Main entry point."""
635
+ def main() -> None:
636
+ """Execute the main CLI application entry point."""
2041
637
  app()
2042
638
 
2043
639