mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

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