mcp-ticketer 0.12.0__py3-none-any.whl ā 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info ā mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info ā mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info ā mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info ā mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -10,24 +10,24 @@ from typing import Any
|
|
|
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
13
|
|
|
15
14
|
# Import adapters module to trigger registration
|
|
16
15
|
import mcp_ticketer.adapters # noqa: F401
|
|
17
16
|
|
|
18
17
|
from ..__version__ import __version__
|
|
19
|
-
from ..core import AdapterRegistry
|
|
20
|
-
from ..core.models import Comment, SearchQuery
|
|
21
|
-
from ..queue import Queue, QueueStatus, WorkerManager
|
|
22
|
-
from ..queue.health_monitor import HealthStatus, QueueHealthMonitor
|
|
23
|
-
from ..queue.ticket_registry import TicketRegistry
|
|
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
|
|
27
23
|
from .instruction_commands import app as instruction_app
|
|
24
|
+
from .mcp_server_commands import mcp_app
|
|
28
25
|
from .migrate_config import migrate_config_command
|
|
29
26
|
from .platform_commands import app as platform_app
|
|
27
|
+
from .platform_installer import install, remove, uninstall
|
|
28
|
+
from .project_update_commands import app as project_update_app
|
|
30
29
|
from .queue_commands import app as queue_app
|
|
30
|
+
from .setup_command import setup
|
|
31
31
|
from .ticket_commands import app as ticket_app
|
|
32
32
|
|
|
33
33
|
# Load environment variables from .env files
|
|
@@ -93,6 +93,7 @@ def load_config(project_dir: Path | None = None) -> dict:
|
|
|
93
93
|
from user home directory or system-wide locations.
|
|
94
94
|
|
|
95
95
|
Args:
|
|
96
|
+
----
|
|
96
97
|
project_dir: Optional project directory to load config from
|
|
97
98
|
|
|
98
99
|
Resolution order:
|
|
@@ -100,6 +101,7 @@ def load_config(project_dir: Path | None = None) -> dict:
|
|
|
100
101
|
2. Default to aitrackdown adapter
|
|
101
102
|
|
|
102
103
|
Returns:
|
|
104
|
+
-------
|
|
103
105
|
Configuration dictionary with adapter and config keys.
|
|
104
106
|
Defaults to aitrackdown if no local config exists.
|
|
105
107
|
|
|
@@ -151,6 +153,7 @@ def _discover_from_env_files() -> str | None:
|
|
|
151
153
|
"""Discover adapter configuration from .env or .env.local files.
|
|
152
154
|
|
|
153
155
|
Returns:
|
|
156
|
+
-------
|
|
154
157
|
Adapter name if discovered, None otherwise
|
|
155
158
|
|
|
156
159
|
"""
|
|
@@ -196,6 +199,7 @@ def _save_adapter_to_config(adapter_name: str) -> None:
|
|
|
196
199
|
"""Save adapter configuration to config file.
|
|
197
200
|
|
|
198
201
|
Args:
|
|
202
|
+
----
|
|
199
203
|
adapter_name: Name of the adapter to save as default
|
|
200
204
|
|
|
201
205
|
"""
|
|
@@ -246,9 +250,11 @@ def merge_config(updates: dict) -> dict:
|
|
|
246
250
|
"""Merge updates into existing config.
|
|
247
251
|
|
|
248
252
|
Args:
|
|
253
|
+
----
|
|
249
254
|
updates: Configuration updates to merge
|
|
250
255
|
|
|
251
256
|
Returns:
|
|
257
|
+
-------
|
|
252
258
|
Updated configuration
|
|
253
259
|
|
|
254
260
|
"""
|
|
@@ -276,6 +282,7 @@ def get_adapter(
|
|
|
276
282
|
"""Get configured adapter instance.
|
|
277
283
|
|
|
278
284
|
Args:
|
|
285
|
+
----
|
|
279
286
|
override_adapter: Override the default adapter type
|
|
280
287
|
override_config: Override configuration for the adapter
|
|
281
288
|
|
|
@@ -303,7 +310,6 @@ def get_adapter(
|
|
|
303
310
|
adapter_config = config["config"]
|
|
304
311
|
|
|
305
312
|
# Add environment variables for authentication
|
|
306
|
-
import os
|
|
307
313
|
|
|
308
314
|
if adapter_type == "linear":
|
|
309
315
|
if not adapter_config.get("api_key"):
|
|
@@ -320,3220 +326,296 @@ def get_adapter(
|
|
|
320
326
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
321
327
|
|
|
322
328
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
Args:
|
|
329
|
-
adapter_type: Type of adapter to validate
|
|
330
|
-
config_file_path: Path to config file
|
|
331
|
-
|
|
332
|
-
Returns:
|
|
333
|
-
List of validation issues (empty if valid)
|
|
334
|
-
|
|
335
|
-
"""
|
|
336
|
-
import json
|
|
337
|
-
|
|
338
|
-
issues = []
|
|
339
|
-
|
|
340
|
-
try:
|
|
341
|
-
# Load config
|
|
342
|
-
with open(config_file_path) as f:
|
|
343
|
-
config = json.load(f)
|
|
344
|
-
|
|
345
|
-
adapter_config = config.get("adapters", {}).get(adapter_type, {})
|
|
346
|
-
|
|
347
|
-
if not adapter_config:
|
|
348
|
-
issues.append(f"No configuration found for {adapter_type}")
|
|
349
|
-
return issues
|
|
350
|
-
|
|
351
|
-
# Validate based on adapter type
|
|
352
|
-
if adapter_type == "linear":
|
|
353
|
-
api_key = adapter_config.get("api_key")
|
|
354
|
-
|
|
355
|
-
# Check API key format
|
|
356
|
-
if not api_key:
|
|
357
|
-
issues.append("Linear API key is missing")
|
|
358
|
-
return issues
|
|
359
|
-
|
|
360
|
-
if not api_key.startswith("lin_api_"):
|
|
361
|
-
issues.append(
|
|
362
|
-
"Invalid Linear API key format (should start with 'lin_api_')"
|
|
363
|
-
)
|
|
364
|
-
return issues
|
|
365
|
-
|
|
366
|
-
# Test actual connectivity
|
|
367
|
-
try:
|
|
368
|
-
from ..adapters.linear import LinearAdapter
|
|
369
|
-
|
|
370
|
-
adapter = LinearAdapter(adapter_config)
|
|
371
|
-
# Try to list one ticket to verify connectivity
|
|
372
|
-
await adapter.list(limit=1)
|
|
373
|
-
except Exception as e:
|
|
374
|
-
error_msg = str(e)
|
|
375
|
-
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
376
|
-
issues.append(
|
|
377
|
-
"Failed to authenticate with Linear API - invalid API key"
|
|
378
|
-
)
|
|
379
|
-
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
380
|
-
issues.append("Linear API key lacks required permissions")
|
|
381
|
-
elif "team" in error_msg.lower():
|
|
382
|
-
issues.append(f"Linear team configuration error: {error_msg}")
|
|
383
|
-
else:
|
|
384
|
-
issues.append(f"Failed to connect to Linear API: {error_msg}")
|
|
385
|
-
|
|
386
|
-
elif adapter_type == "jira":
|
|
387
|
-
server = adapter_config.get("server")
|
|
388
|
-
email = adapter_config.get("email")
|
|
389
|
-
api_token = adapter_config.get("api_token")
|
|
390
|
-
|
|
391
|
-
# Check required fields
|
|
392
|
-
if not server:
|
|
393
|
-
issues.append("JIRA server URL is missing")
|
|
394
|
-
if not email:
|
|
395
|
-
issues.append("JIRA email is missing")
|
|
396
|
-
if not api_token:
|
|
397
|
-
issues.append("JIRA API token is missing")
|
|
398
|
-
|
|
399
|
-
if issues:
|
|
400
|
-
return issues
|
|
401
|
-
|
|
402
|
-
# Test actual connectivity
|
|
403
|
-
try:
|
|
404
|
-
from ..adapters.jira import JiraAdapter
|
|
405
|
-
|
|
406
|
-
adapter = JiraAdapter(adapter_config)
|
|
407
|
-
await adapter.list(limit=1)
|
|
408
|
-
except Exception as e:
|
|
409
|
-
error_msg = str(e)
|
|
410
|
-
if "401" in error_msg or "Unauthorized" in error_msg:
|
|
411
|
-
issues.append(
|
|
412
|
-
"Failed to authenticate with JIRA - invalid credentials"
|
|
413
|
-
)
|
|
414
|
-
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
415
|
-
issues.append("JIRA credentials lack required permissions")
|
|
416
|
-
else:
|
|
417
|
-
issues.append(f"Failed to connect to JIRA: {error_msg}")
|
|
418
|
-
|
|
419
|
-
elif adapter_type == "github":
|
|
420
|
-
token = adapter_config.get("token") or adapter_config.get("api_key")
|
|
421
|
-
owner = adapter_config.get("owner")
|
|
422
|
-
repo = adapter_config.get("repo")
|
|
423
|
-
|
|
424
|
-
# Check required fields
|
|
425
|
-
if not token:
|
|
426
|
-
issues.append("GitHub token is missing")
|
|
427
|
-
if not owner:
|
|
428
|
-
issues.append("GitHub owner is missing")
|
|
429
|
-
if not repo:
|
|
430
|
-
issues.append("GitHub repo is missing")
|
|
431
|
-
|
|
432
|
-
if issues:
|
|
433
|
-
return issues
|
|
434
|
-
|
|
435
|
-
# Test actual connectivity
|
|
436
|
-
try:
|
|
437
|
-
from ..adapters.github import GitHubAdapter
|
|
438
|
-
|
|
439
|
-
adapter = GitHubAdapter(adapter_config)
|
|
440
|
-
await adapter.list(limit=1)
|
|
441
|
-
except Exception as e:
|
|
442
|
-
error_msg = str(e)
|
|
443
|
-
if (
|
|
444
|
-
"401" in error_msg
|
|
445
|
-
or "Unauthorized" in error_msg
|
|
446
|
-
or "Bad credentials" in error_msg
|
|
447
|
-
):
|
|
448
|
-
issues.append("Failed to authenticate with GitHub - invalid token")
|
|
449
|
-
elif "404" in error_msg or "Not Found" in error_msg:
|
|
450
|
-
issues.append(f"GitHub repository not found: {owner}/{repo}")
|
|
451
|
-
elif "403" in error_msg or "Forbidden" in error_msg:
|
|
452
|
-
issues.append("GitHub token lacks required permissions")
|
|
453
|
-
else:
|
|
454
|
-
issues.append(f"Failed to connect to GitHub: {error_msg}")
|
|
455
|
-
|
|
456
|
-
elif adapter_type == "aitrackdown":
|
|
457
|
-
# AITrackdown doesn't require credentials, just check base_path is set
|
|
458
|
-
base_path = adapter_config.get("base_path")
|
|
459
|
-
if not base_path:
|
|
460
|
-
issues.append("AITrackdown base_path is missing")
|
|
461
|
-
|
|
462
|
-
except Exception as e:
|
|
463
|
-
issues.append(f"Validation error: {str(e)}")
|
|
464
|
-
|
|
465
|
-
return issues
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
async def _validate_configuration_with_retry(
|
|
469
|
-
console: Console, adapter_type: str, config_file_path: Path, proj_path: Path
|
|
470
|
-
) -> bool:
|
|
471
|
-
"""Validate configuration with retry loop for corrections.
|
|
472
|
-
|
|
473
|
-
Args:
|
|
474
|
-
console: Rich console for output
|
|
475
|
-
adapter_type: Type of adapter configured
|
|
476
|
-
config_file_path: Path to config file
|
|
477
|
-
proj_path: Project path
|
|
478
|
-
|
|
479
|
-
Returns:
|
|
480
|
-
True if validation passed or user chose to continue, False if user chose to exit
|
|
481
|
-
|
|
482
|
-
"""
|
|
483
|
-
max_retries = 3
|
|
484
|
-
retry_count = 0
|
|
485
|
-
|
|
486
|
-
while retry_count < max_retries:
|
|
487
|
-
console.print("\n[cyan]š Validating configuration...[/cyan]")
|
|
488
|
-
|
|
489
|
-
# Run real adapter validation (suppress verbose output)
|
|
490
|
-
import io
|
|
491
|
-
import sys
|
|
492
|
-
|
|
493
|
-
# Capture output to suppress verbose diagnostics output
|
|
494
|
-
old_stdout = sys.stdout
|
|
495
|
-
old_stderr = sys.stderr
|
|
496
|
-
sys.stdout = io.StringIO()
|
|
497
|
-
sys.stderr = io.StringIO()
|
|
498
|
-
|
|
499
|
-
try:
|
|
500
|
-
# Perform real adapter validation using diagnostics
|
|
501
|
-
validation_issues = await _validate_adapter_credentials(
|
|
502
|
-
adapter_type, config_file_path
|
|
503
|
-
)
|
|
504
|
-
finally:
|
|
505
|
-
# Restore stdout/stderr
|
|
506
|
-
sys.stdout = old_stdout
|
|
507
|
-
sys.stderr = old_stderr
|
|
508
|
-
|
|
509
|
-
# Check if there are issues
|
|
510
|
-
if not validation_issues:
|
|
511
|
-
console.print("[green]ā Configuration validated successfully![/green]")
|
|
512
|
-
return True
|
|
513
|
-
|
|
514
|
-
# Display issues found
|
|
515
|
-
console.print("[yellow]ā ļø Configuration validation found issues:[/yellow]")
|
|
516
|
-
for issue in validation_issues:
|
|
517
|
-
console.print(f" [red]ā[/red] {issue}")
|
|
518
|
-
|
|
519
|
-
# Offer user options
|
|
520
|
-
console.print("\n[bold]What would you like to do?[/bold]")
|
|
521
|
-
console.print("1. [cyan]Re-enter configuration values[/cyan] (fix issues)")
|
|
522
|
-
console.print("2. [yellow]Continue anyway[/yellow] (skip validation)")
|
|
523
|
-
console.print("3. [red]Exit[/red] (fix manually later)")
|
|
524
|
-
|
|
525
|
-
try:
|
|
526
|
-
choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
|
|
527
|
-
except typer.Abort:
|
|
528
|
-
console.print("[yellow]Cancelled.[/yellow]")
|
|
529
|
-
return False
|
|
530
|
-
|
|
531
|
-
if choice == 1:
|
|
532
|
-
# Re-enter configuration
|
|
533
|
-
# Check BEFORE increment to fix off-by-one error
|
|
534
|
-
if retry_count >= max_retries:
|
|
535
|
-
console.print(
|
|
536
|
-
f"[red]Maximum retry attempts ({max_retries}) reached.[/red]"
|
|
537
|
-
)
|
|
538
|
-
console.print(
|
|
539
|
-
"[yellow]Please fix configuration manually and run 'mcp-ticketer doctor'[/yellow]"
|
|
540
|
-
)
|
|
541
|
-
return False
|
|
542
|
-
retry_count += 1
|
|
543
|
-
|
|
544
|
-
console.print(
|
|
545
|
-
f"\n[cyan]Retry {retry_count}/{max_retries} - Re-entering configuration...[/cyan]"
|
|
546
|
-
)
|
|
547
|
-
|
|
548
|
-
# Reload current config to get values
|
|
549
|
-
import json
|
|
550
|
-
|
|
551
|
-
with open(config_file_path) as f:
|
|
552
|
-
current_config = json.load(f)
|
|
553
|
-
|
|
554
|
-
# Re-prompt for adapter-specific configuration
|
|
555
|
-
if adapter_type == "linear":
|
|
556
|
-
console.print("\n[bold]Linear Configuration[/bold]")
|
|
557
|
-
console.print(
|
|
558
|
-
"[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
|
|
559
|
-
)
|
|
560
|
-
|
|
561
|
-
linear_api_key = typer.prompt(
|
|
562
|
-
"Enter your Linear API key", hide_input=True
|
|
563
|
-
)
|
|
564
|
-
|
|
565
|
-
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
566
|
-
console.print("You can provide either:")
|
|
567
|
-
console.print(
|
|
568
|
-
" 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
|
|
569
|
-
)
|
|
570
|
-
console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
|
|
571
|
-
console.print(" 3. Team ID (UUID)")
|
|
572
|
-
console.print(
|
|
573
|
-
"[dim]Find team URL or key in: Linear ā Your Team ā Team Issues Page[/dim]\n"
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
team_input = typer.prompt("Team URL, key, or ID")
|
|
577
|
-
|
|
578
|
-
# Check if input is a URL
|
|
579
|
-
linear_team_id = None
|
|
580
|
-
linear_team_key = None
|
|
581
|
-
|
|
582
|
-
if team_input.startswith("https://linear.app/"):
|
|
583
|
-
console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
|
|
584
|
-
from .linear_commands import derive_team_from_url
|
|
585
|
-
|
|
586
|
-
derived_team_id, error = await derive_team_from_url(
|
|
587
|
-
linear_api_key, team_input
|
|
588
|
-
)
|
|
589
|
-
|
|
590
|
-
if derived_team_id:
|
|
591
|
-
linear_team_id = derived_team_id
|
|
592
|
-
console.print(
|
|
593
|
-
"[green]ā[/green] Successfully derived team ID from URL"
|
|
594
|
-
)
|
|
595
|
-
else:
|
|
596
|
-
console.print(f"[red]Error:[/red] {error}")
|
|
597
|
-
console.print("Please provide team key or ID manually instead.")
|
|
598
|
-
team_input = typer.prompt("Team key or ID")
|
|
599
|
-
|
|
600
|
-
if len(team_input) > 20: # Likely a UUID
|
|
601
|
-
linear_team_id = team_input
|
|
602
|
-
else:
|
|
603
|
-
linear_team_key = team_input
|
|
604
|
-
else:
|
|
605
|
-
# Input is team key or ID
|
|
606
|
-
if len(team_input) > 20: # Likely a UUID
|
|
607
|
-
linear_team_id = team_input
|
|
608
|
-
else:
|
|
609
|
-
linear_team_key = team_input
|
|
610
|
-
|
|
611
|
-
# Update config
|
|
612
|
-
linear_config = {
|
|
613
|
-
"api_key": linear_api_key,
|
|
614
|
-
"type": "linear",
|
|
615
|
-
}
|
|
616
|
-
if linear_team_key:
|
|
617
|
-
linear_config["team_key"] = linear_team_key
|
|
618
|
-
if linear_team_id:
|
|
619
|
-
linear_config["team_id"] = linear_team_id
|
|
620
|
-
|
|
621
|
-
current_config["adapters"]["linear"] = linear_config
|
|
622
|
-
|
|
623
|
-
elif adapter_type == "jira":
|
|
624
|
-
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
625
|
-
console.print("Enter your JIRA server details.\n")
|
|
626
|
-
|
|
627
|
-
server = typer.prompt(
|
|
628
|
-
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
629
|
-
)
|
|
630
|
-
email = typer.prompt("Your JIRA email address")
|
|
631
|
-
|
|
632
|
-
console.print("\nYou need a JIRA API token.")
|
|
633
|
-
console.print(
|
|
634
|
-
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
638
|
-
|
|
639
|
-
project = typer.prompt(
|
|
640
|
-
"Default JIRA project key (optional, press Enter to skip)",
|
|
641
|
-
default="",
|
|
642
|
-
show_default=False,
|
|
643
|
-
)
|
|
644
|
-
|
|
645
|
-
# Update config
|
|
646
|
-
jira_config = {
|
|
647
|
-
"server": server,
|
|
648
|
-
"email": email,
|
|
649
|
-
"api_token": token,
|
|
650
|
-
"type": "jira",
|
|
651
|
-
}
|
|
652
|
-
if project:
|
|
653
|
-
jira_config["project_key"] = project
|
|
654
|
-
|
|
655
|
-
current_config["adapters"]["jira"] = jira_config
|
|
656
|
-
|
|
657
|
-
elif adapter_type == "github":
|
|
658
|
-
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
659
|
-
console.print("Enter your GitHub repository details.\n")
|
|
660
|
-
|
|
661
|
-
owner = typer.prompt(
|
|
662
|
-
"GitHub repository owner (username or organization)"
|
|
663
|
-
)
|
|
664
|
-
repo = typer.prompt("GitHub repository name")
|
|
665
|
-
|
|
666
|
-
console.print("\nYou need a GitHub Personal Access Token.")
|
|
667
|
-
console.print(
|
|
668
|
-
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
669
|
-
)
|
|
670
|
-
console.print(
|
|
671
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
|
|
672
|
-
)
|
|
673
|
-
|
|
674
|
-
token = typer.prompt(
|
|
675
|
-
"Enter your GitHub Personal Access Token", hide_input=True
|
|
676
|
-
)
|
|
677
|
-
|
|
678
|
-
# Update config
|
|
679
|
-
current_config["adapters"]["github"] = {
|
|
680
|
-
"owner": owner,
|
|
681
|
-
"repo": repo,
|
|
682
|
-
"token": token,
|
|
683
|
-
"type": "github",
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
elif adapter_type == "aitrackdown":
|
|
687
|
-
# AITrackdown doesn't need credentials, but save config before returning
|
|
688
|
-
# Save updated configuration
|
|
689
|
-
with open(config_file_path, "w") as f:
|
|
690
|
-
json.dump(current_config, f, indent=2)
|
|
691
|
-
|
|
692
|
-
console.print(
|
|
693
|
-
"[yellow]AITrackdown doesn't require credentials. Continuing...[/yellow]"
|
|
694
|
-
)
|
|
695
|
-
console.print("[dim]ā Configuration updated[/dim]")
|
|
696
|
-
return True
|
|
697
|
-
|
|
698
|
-
else:
|
|
699
|
-
console.print(f"[red]Unknown adapter type: {adapter_type}[/red]")
|
|
700
|
-
return False
|
|
701
|
-
|
|
702
|
-
# Save updated configuration
|
|
703
|
-
with open(config_file_path, "w") as f:
|
|
704
|
-
json.dump(current_config, f, indent=2)
|
|
705
|
-
|
|
706
|
-
console.print("[dim]ā Configuration updated[/dim]")
|
|
707
|
-
# Loop will retry validation
|
|
708
|
-
|
|
709
|
-
elif choice == 2:
|
|
710
|
-
# Continue anyway
|
|
711
|
-
console.print(
|
|
712
|
-
"[yellow]ā ļø Continuing with potentially invalid configuration.[/yellow]"
|
|
713
|
-
)
|
|
714
|
-
console.print("[dim]You can validate later with: mcp-ticketer doctor[/dim]")
|
|
715
|
-
return True
|
|
716
|
-
|
|
717
|
-
elif choice == 3:
|
|
718
|
-
# Exit
|
|
719
|
-
console.print(
|
|
720
|
-
"[yellow]Configuration saved but not validated. Run 'mcp-ticketer doctor' to test.[/yellow]"
|
|
721
|
-
)
|
|
722
|
-
return False
|
|
723
|
-
|
|
724
|
-
else:
|
|
725
|
-
console.print(
|
|
726
|
-
f"[red]Invalid choice: {choice}. Please enter 1, 2, or 3.[/red]"
|
|
727
|
-
)
|
|
728
|
-
# Continue loop to ask again
|
|
729
|
-
|
|
730
|
-
return True
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
734
|
-
"""Interactive prompt for adapter selection.
|
|
735
|
-
|
|
736
|
-
Args:
|
|
737
|
-
console: Rich console for output
|
|
738
|
-
|
|
739
|
-
Returns:
|
|
740
|
-
Selected adapter type
|
|
741
|
-
|
|
742
|
-
"""
|
|
743
|
-
console.print("\n[bold blue]š MCP Ticketer Setup[/bold blue]")
|
|
744
|
-
console.print("Choose which ticket system you want to connect to:\n")
|
|
745
|
-
|
|
746
|
-
# Define adapter options with descriptions
|
|
747
|
-
adapters = [
|
|
748
|
-
{
|
|
749
|
-
"name": "linear",
|
|
750
|
-
"title": "Linear",
|
|
751
|
-
"description": "Modern project management (linear.app)",
|
|
752
|
-
"requirements": "API key and team ID",
|
|
753
|
-
},
|
|
754
|
-
{
|
|
755
|
-
"name": "github",
|
|
756
|
-
"title": "GitHub Issues",
|
|
757
|
-
"description": "GitHub repository issues",
|
|
758
|
-
"requirements": "Personal access token, owner, and repo",
|
|
759
|
-
},
|
|
760
|
-
{
|
|
761
|
-
"name": "jira",
|
|
762
|
-
"title": "JIRA",
|
|
763
|
-
"description": "Atlassian JIRA project management",
|
|
764
|
-
"requirements": "Server URL, email, and API token",
|
|
765
|
-
},
|
|
766
|
-
{
|
|
767
|
-
"name": "aitrackdown",
|
|
768
|
-
"title": "Local Files (AITrackdown)",
|
|
769
|
-
"description": "Store tickets in local files (no external service)",
|
|
770
|
-
"requirements": "None - works offline",
|
|
771
|
-
},
|
|
772
|
-
]
|
|
773
|
-
|
|
774
|
-
# Display options
|
|
775
|
-
for i, adapter in enumerate(adapters, 1):
|
|
776
|
-
console.print(f"[cyan]{i}.[/cyan] [bold]{adapter['title']}[/bold]")
|
|
777
|
-
console.print(f" {adapter['description']}")
|
|
778
|
-
console.print(f" [dim]Requirements: {adapter['requirements']}[/dim]\n")
|
|
779
|
-
|
|
780
|
-
# Get user selection
|
|
781
|
-
while True:
|
|
782
|
-
try:
|
|
783
|
-
choice = typer.prompt("Select adapter (1-4)", type=int, default=1)
|
|
784
|
-
if 1 <= choice <= len(adapters):
|
|
785
|
-
selected_adapter = adapters[choice - 1]
|
|
786
|
-
console.print(
|
|
787
|
-
f"\n[green]ā Selected: {selected_adapter['title']}[/green]"
|
|
788
|
-
)
|
|
789
|
-
return selected_adapter["name"]
|
|
790
|
-
else:
|
|
791
|
-
console.print(
|
|
792
|
-
f"[red]Please enter a number between 1 and {len(adapters)}[/red]"
|
|
793
|
-
)
|
|
794
|
-
except (ValueError, typer.Abort):
|
|
795
|
-
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
796
|
-
raise typer.Exit(0) from None
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
@app.command()
|
|
800
|
-
def setup(
|
|
801
|
-
project_path: str | None = typer.Option(
|
|
802
|
-
None, "--path", help="Project path (default: current directory)"
|
|
803
|
-
),
|
|
804
|
-
skip_platforms: bool = typer.Option(
|
|
805
|
-
False,
|
|
806
|
-
"--skip-platforms",
|
|
807
|
-
help="Skip platform installation (only initialize adapter)",
|
|
808
|
-
),
|
|
809
|
-
force_reinit: bool = typer.Option(
|
|
810
|
-
False,
|
|
811
|
-
"--force-reinit",
|
|
812
|
-
help="Force re-initialization even if config exists",
|
|
813
|
-
),
|
|
814
|
-
) -> None:
|
|
815
|
-
"""Smart setup command - combines init + platform installation.
|
|
816
|
-
|
|
817
|
-
This command intelligently detects your current setup state and only
|
|
818
|
-
performs necessary configuration. It's the recommended way to get started.
|
|
819
|
-
|
|
820
|
-
Detection & Smart Actions:
|
|
821
|
-
- First run: Full setup (init + platform installation)
|
|
822
|
-
- Existing config: Skip init, offer platform installation
|
|
823
|
-
- Detects changes: Offers to update configurations
|
|
824
|
-
- Respects existing: Won't overwrite without confirmation
|
|
825
|
-
|
|
826
|
-
Examples:
|
|
827
|
-
# Smart setup (recommended for first-time setup)
|
|
828
|
-
mcp-ticketer setup
|
|
829
|
-
|
|
830
|
-
# Setup for different project
|
|
831
|
-
mcp-ticketer setup --path /path/to/project
|
|
832
|
-
|
|
833
|
-
# Re-initialize configuration
|
|
834
|
-
mcp-ticketer setup --force-reinit
|
|
835
|
-
|
|
836
|
-
# Only init adapter, skip platform installation
|
|
837
|
-
mcp-ticketer setup --skip-platforms
|
|
838
|
-
|
|
839
|
-
Note: For advanced configuration, use 'init' and 'install' separately.
|
|
840
|
-
|
|
841
|
-
"""
|
|
842
|
-
from .platform_detection import PlatformDetector
|
|
843
|
-
|
|
844
|
-
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
845
|
-
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
846
|
-
|
|
847
|
-
console.print("[bold cyan]š MCP Ticketer Smart Setup[/bold cyan]\n")
|
|
848
|
-
|
|
849
|
-
# Step 1: Detect existing configuration
|
|
850
|
-
config_exists = config_path.exists()
|
|
851
|
-
config_valid = False
|
|
852
|
-
current_adapter = None
|
|
853
|
-
|
|
854
|
-
if config_exists and not force_reinit:
|
|
855
|
-
try:
|
|
856
|
-
with open(config_path) as f:
|
|
857
|
-
config = json.load(f)
|
|
858
|
-
current_adapter = config.get("default_adapter")
|
|
859
|
-
config_valid = bool(current_adapter and config.get("adapters"))
|
|
860
|
-
except (json.JSONDecodeError, OSError):
|
|
861
|
-
config_valid = False
|
|
862
|
-
|
|
863
|
-
if config_valid:
|
|
864
|
-
console.print("[green]ā[/green] Configuration detected")
|
|
865
|
-
console.print(f"[dim] Adapter: {current_adapter}[/dim]")
|
|
866
|
-
console.print(f"[dim] Location: {config_path}[/dim]\n")
|
|
867
|
-
|
|
868
|
-
# Offer to reconfigure
|
|
869
|
-
if not typer.confirm(
|
|
870
|
-
"Configuration already exists. Keep existing settings?", default=True
|
|
871
|
-
):
|
|
872
|
-
console.print("[cyan]Re-initializing configuration...[/cyan]\n")
|
|
873
|
-
force_reinit = True
|
|
874
|
-
config_valid = False
|
|
875
|
-
else:
|
|
876
|
-
if config_exists:
|
|
877
|
-
console.print(
|
|
878
|
-
"[yellow]ā [/yellow] Configuration file exists but is invalid\n"
|
|
879
|
-
)
|
|
880
|
-
else:
|
|
881
|
-
console.print("[yellow]ā [/yellow] No configuration found\n")
|
|
882
|
-
|
|
883
|
-
# Step 2: Initialize adapter configuration if needed
|
|
884
|
-
if not config_valid or force_reinit:
|
|
885
|
-
console.print("[bold]Step 1/2: Adapter Configuration[/bold]\n")
|
|
886
|
-
|
|
887
|
-
# Run init command non-interactively through function call
|
|
888
|
-
# We'll use the discover and prompt flow from init
|
|
889
|
-
from ..core.env_discovery import discover_config
|
|
890
|
-
|
|
891
|
-
discovered = discover_config(proj_path)
|
|
892
|
-
adapter_type = None
|
|
893
|
-
|
|
894
|
-
# Try auto-discovery
|
|
895
|
-
if discovered and discovered.adapters:
|
|
896
|
-
primary = discovered.get_primary_adapter()
|
|
897
|
-
if primary:
|
|
898
|
-
adapter_type = primary.adapter_type
|
|
899
|
-
console.print(f"[green]ā Auto-detected {adapter_type} adapter[/green]")
|
|
900
|
-
console.print(f"[dim] Source: {primary.found_in}[/dim]")
|
|
901
|
-
console.print(f"[dim] Confidence: {primary.confidence:.0%}[/dim]\n")
|
|
902
|
-
|
|
903
|
-
if not typer.confirm(
|
|
904
|
-
f"Use detected {adapter_type} adapter?", default=True
|
|
905
|
-
):
|
|
906
|
-
adapter_type = None
|
|
907
|
-
|
|
908
|
-
# If no adapter detected, prompt for selection
|
|
909
|
-
if not adapter_type:
|
|
910
|
-
adapter_type = _prompt_for_adapter_selection(console)
|
|
911
|
-
|
|
912
|
-
# Now run the full init with the selected adapter
|
|
913
|
-
console.print(f"\n[cyan]Initializing {adapter_type} adapter...[/cyan]\n")
|
|
914
|
-
|
|
915
|
-
# Call init programmatically
|
|
916
|
-
init(
|
|
917
|
-
adapter=adapter_type,
|
|
918
|
-
project_path=str(proj_path),
|
|
919
|
-
global_config=False,
|
|
920
|
-
)
|
|
921
|
-
|
|
922
|
-
console.print("\n[green]ā Adapter configuration complete[/green]\n")
|
|
923
|
-
else:
|
|
924
|
-
console.print("[green]ā Step 1/2: Adapter already configured[/green]\n")
|
|
925
|
-
|
|
926
|
-
# Step 3: Platform installation
|
|
927
|
-
if skip_platforms:
|
|
928
|
-
console.print(
|
|
929
|
-
"[yellow]ā [/yellow] Skipping platform installation (--skip-platforms)\n"
|
|
930
|
-
)
|
|
931
|
-
_show_setup_complete_message(console, proj_path)
|
|
932
|
-
return
|
|
933
|
-
|
|
934
|
-
console.print("[bold]Step 2/2: Platform Installation[/bold]\n")
|
|
935
|
-
|
|
936
|
-
# Detect available platforms
|
|
937
|
-
detector = PlatformDetector()
|
|
938
|
-
detected = detector.detect_all(project_path=proj_path)
|
|
939
|
-
|
|
940
|
-
if not detected:
|
|
941
|
-
console.print("[yellow]No AI platforms detected on this system.[/yellow]")
|
|
942
|
-
console.print(
|
|
943
|
-
"\n[dim]Supported platforms: Claude Code, Claude Desktop, Gemini, Codex, Auggie[/dim]"
|
|
944
|
-
)
|
|
945
|
-
console.print(
|
|
946
|
-
"[dim]Install these platforms to use them with mcp-ticketer.[/dim]\n"
|
|
947
|
-
)
|
|
948
|
-
_show_setup_complete_message(console, proj_path)
|
|
949
|
-
return
|
|
950
|
-
|
|
951
|
-
# Filter to only installed platforms
|
|
952
|
-
installed = [p for p in detected if p.is_installed]
|
|
953
|
-
|
|
954
|
-
if not installed:
|
|
955
|
-
console.print(
|
|
956
|
-
"[yellow]AI platforms detected but have configuration issues.[/yellow]"
|
|
957
|
-
)
|
|
958
|
-
console.print(
|
|
959
|
-
"\n[dim]Run 'mcp-ticketer install --auto-detect' for details.[/dim]\n"
|
|
960
|
-
)
|
|
961
|
-
_show_setup_complete_message(console, proj_path)
|
|
962
|
-
return
|
|
963
|
-
|
|
964
|
-
# Show detected platforms
|
|
965
|
-
console.print(f"[green]ā[/green] Detected {len(installed)} platform(s):\n")
|
|
966
|
-
for plat in installed:
|
|
967
|
-
console.print(f" ⢠{plat.display_name} ({plat.scope})")
|
|
968
|
-
|
|
969
|
-
console.print()
|
|
970
|
-
|
|
971
|
-
# Check if mcp-ticketer is already configured for these platforms
|
|
972
|
-
already_configured = _check_existing_platform_configs(installed, proj_path)
|
|
973
|
-
|
|
974
|
-
if already_configured:
|
|
975
|
-
console.print(
|
|
976
|
-
f"[green]ā[/green] mcp-ticketer already configured for {len(already_configured)} platform(s)\n"
|
|
977
|
-
)
|
|
978
|
-
for plat_name in already_configured:
|
|
979
|
-
console.print(f" ⢠{plat_name}")
|
|
980
|
-
console.print()
|
|
981
|
-
|
|
982
|
-
if not typer.confirm("Update platform configurations anyway?", default=False):
|
|
983
|
-
console.print("[yellow]Skipping platform installation[/yellow]\n")
|
|
984
|
-
_show_setup_complete_message(console, proj_path)
|
|
985
|
-
return
|
|
986
|
-
|
|
987
|
-
# Offer to install for all or select specific
|
|
988
|
-
console.print("[bold]Platform Installation Options:[/bold]")
|
|
989
|
-
console.print("1. Install for all detected platforms")
|
|
990
|
-
console.print("2. Select specific platform")
|
|
991
|
-
console.print("3. Skip platform installation")
|
|
992
|
-
|
|
993
|
-
try:
|
|
994
|
-
choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
|
|
995
|
-
except typer.Abort:
|
|
996
|
-
console.print("[yellow]Setup cancelled[/yellow]")
|
|
997
|
-
raise typer.Exit(0) from None
|
|
998
|
-
|
|
999
|
-
if choice == 3:
|
|
1000
|
-
console.print("[yellow]Skipping platform installation[/yellow]\n")
|
|
1001
|
-
_show_setup_complete_message(console, proj_path)
|
|
1002
|
-
return
|
|
1003
|
-
|
|
1004
|
-
# Import configuration functions
|
|
1005
|
-
from .auggie_configure import configure_auggie_mcp
|
|
1006
|
-
from .codex_configure import configure_codex_mcp
|
|
1007
|
-
from .gemini_configure import configure_gemini_mcp
|
|
1008
|
-
from .mcp_configure import configure_claude_mcp
|
|
1009
|
-
|
|
1010
|
-
platform_mapping = {
|
|
1011
|
-
"claude-code": lambda: configure_claude_mcp(global_config=False, force=True),
|
|
1012
|
-
"claude-desktop": lambda: configure_claude_mcp(global_config=True, force=True),
|
|
1013
|
-
"auggie": lambda: configure_auggie_mcp(force=True),
|
|
1014
|
-
"gemini": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
1015
|
-
"codex": lambda: configure_codex_mcp(force=True),
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
platforms_to_install = []
|
|
1019
|
-
|
|
1020
|
-
if choice == 1:
|
|
1021
|
-
# Install for all
|
|
1022
|
-
platforms_to_install = installed
|
|
1023
|
-
elif choice == 2:
|
|
1024
|
-
# Select specific platform
|
|
1025
|
-
console.print("\n[bold]Select platform:[/bold]")
|
|
1026
|
-
for idx, plat in enumerate(installed, 1):
|
|
1027
|
-
console.print(f" {idx}. {plat.display_name} ({plat.scope})")
|
|
1028
|
-
|
|
1029
|
-
try:
|
|
1030
|
-
plat_choice = typer.prompt("\nSelect platform number", type=int)
|
|
1031
|
-
if 1 <= plat_choice <= len(installed):
|
|
1032
|
-
platforms_to_install = [installed[plat_choice - 1]]
|
|
1033
|
-
else:
|
|
1034
|
-
console.print("[red]Invalid selection[/red]")
|
|
1035
|
-
raise typer.Exit(1) from None
|
|
1036
|
-
except typer.Abort:
|
|
1037
|
-
console.print("[yellow]Setup cancelled[/yellow]")
|
|
1038
|
-
raise typer.Exit(0) from None
|
|
1039
|
-
|
|
1040
|
-
# Install for selected platforms
|
|
1041
|
-
console.print()
|
|
1042
|
-
success_count = 0
|
|
1043
|
-
failed = []
|
|
1044
|
-
|
|
1045
|
-
for plat in platforms_to_install:
|
|
1046
|
-
config_func = platform_mapping.get(plat.name)
|
|
1047
|
-
if not config_func:
|
|
1048
|
-
console.print(f"[yellow]ā [/yellow] No installer for {plat.display_name}")
|
|
1049
|
-
continue
|
|
1050
|
-
|
|
1051
|
-
try:
|
|
1052
|
-
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
1053
|
-
config_func()
|
|
1054
|
-
console.print(f"[green]ā[/green] {plat.display_name} configured\n")
|
|
1055
|
-
success_count += 1
|
|
1056
|
-
except Exception as e:
|
|
1057
|
-
console.print(
|
|
1058
|
-
f"[red]ā[/red] Failed to configure {plat.display_name}: {e}\n"
|
|
1059
|
-
)
|
|
1060
|
-
failed.append(plat.display_name)
|
|
1061
|
-
|
|
1062
|
-
# Summary
|
|
1063
|
-
console.print(
|
|
1064
|
-
f"[bold]Platform Installation:[/bold] {success_count}/{len(platforms_to_install)} succeeded"
|
|
1065
|
-
)
|
|
1066
|
-
if failed:
|
|
1067
|
-
console.print(f"[red]Failed:[/red] {', '.join(failed)}")
|
|
1068
|
-
|
|
1069
|
-
console.print()
|
|
1070
|
-
_show_setup_complete_message(console, proj_path)
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
def _check_existing_platform_configs(platforms: list, proj_path: Path) -> list[str]:
|
|
1074
|
-
"""Check if mcp-ticketer is already configured for given platforms.
|
|
1075
|
-
|
|
1076
|
-
Args:
|
|
1077
|
-
platforms: List of DetectedPlatform objects
|
|
1078
|
-
proj_path: Project path
|
|
1079
|
-
|
|
1080
|
-
Returns:
|
|
1081
|
-
List of platform display names that are already configured
|
|
1082
|
-
|
|
1083
|
-
"""
|
|
1084
|
-
configured = []
|
|
1085
|
-
|
|
1086
|
-
for plat in platforms:
|
|
1087
|
-
try:
|
|
1088
|
-
if plat.name == "claude-code":
|
|
1089
|
-
config_path = Path.home() / ".claude.json"
|
|
1090
|
-
if config_path.exists():
|
|
1091
|
-
with open(config_path) as f:
|
|
1092
|
-
config = json.load(f)
|
|
1093
|
-
projects = config.get("projects", {})
|
|
1094
|
-
proj_key = str(proj_path)
|
|
1095
|
-
if proj_key in projects:
|
|
1096
|
-
mcp_servers = projects[proj_key].get("mcpServers", {})
|
|
1097
|
-
if "mcp-ticketer" in mcp_servers:
|
|
1098
|
-
configured.append(plat.display_name)
|
|
1099
|
-
|
|
1100
|
-
elif plat.name == "claude-desktop":
|
|
1101
|
-
if plat.config_path.exists():
|
|
1102
|
-
with open(plat.config_path) as f:
|
|
1103
|
-
config = json.load(f)
|
|
1104
|
-
if "mcp-ticketer" in config.get("mcpServers", {}):
|
|
1105
|
-
configured.append(plat.display_name)
|
|
1106
|
-
|
|
1107
|
-
elif plat.name in ["auggie", "codex", "gemini"]:
|
|
1108
|
-
if plat.config_path.exists():
|
|
1109
|
-
# Check if mcp-ticketer is configured
|
|
1110
|
-
# Implementation depends on each platform's config format
|
|
1111
|
-
# For now, just check if config exists (simplified)
|
|
1112
|
-
pass
|
|
1113
|
-
|
|
1114
|
-
except (json.JSONDecodeError, OSError):
|
|
1115
|
-
pass
|
|
1116
|
-
|
|
1117
|
-
return configured
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
def _show_setup_complete_message(console: Console, proj_path: Path) -> None:
|
|
1121
|
-
"""Show setup complete message with next steps.
|
|
1122
|
-
|
|
1123
|
-
Args:
|
|
1124
|
-
console: Rich console for output
|
|
1125
|
-
proj_path: Project path
|
|
1126
|
-
|
|
1127
|
-
"""
|
|
1128
|
-
console.print("[bold green]š Setup Complete![/bold green]\n")
|
|
1129
|
-
|
|
1130
|
-
console.print("[bold]Quick Start:[/bold]")
|
|
1131
|
-
console.print("1. Create a test ticket:")
|
|
1132
|
-
console.print(" [cyan]mcp-ticketer create 'My first ticket'[/cyan]\n")
|
|
1133
|
-
|
|
1134
|
-
console.print("2. List tickets:")
|
|
1135
|
-
console.print(" [cyan]mcp-ticketer list[/cyan]\n")
|
|
1136
|
-
|
|
1137
|
-
console.print("[bold]Useful Commands:[/bold]")
|
|
1138
|
-
console.print(" [cyan]mcp-ticketer doctor[/cyan] - Validate configuration")
|
|
1139
|
-
console.print(" [cyan]mcp-ticketer install <platform>[/cyan] - Add more platforms")
|
|
1140
|
-
console.print(" [cyan]mcp-ticketer --help[/cyan] - See all commands\n")
|
|
1141
|
-
|
|
1142
|
-
console.print(
|
|
1143
|
-
f"[dim]Configuration: {proj_path / '.mcp-ticketer' / 'config.json'}[/dim]"
|
|
1144
|
-
)
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
@app.command()
|
|
1148
|
-
def init(
|
|
1149
|
-
adapter: str | None = typer.Option(
|
|
1150
|
-
None,
|
|
1151
|
-
"--adapter",
|
|
1152
|
-
"-a",
|
|
1153
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
1154
|
-
),
|
|
1155
|
-
project_path: str | None = typer.Option(
|
|
1156
|
-
None, "--path", help="Project path (default: current directory)"
|
|
1157
|
-
),
|
|
1158
|
-
global_config: bool = typer.Option(
|
|
1159
|
-
False,
|
|
1160
|
-
"--global",
|
|
1161
|
-
"-g",
|
|
1162
|
-
help="Save to global config instead of project-specific",
|
|
1163
|
-
),
|
|
1164
|
-
base_path: str | None = typer.Option(
|
|
1165
|
-
None,
|
|
1166
|
-
"--base-path",
|
|
1167
|
-
"-p",
|
|
1168
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
1169
|
-
),
|
|
1170
|
-
api_key: str | None = typer.Option(
|
|
1171
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
1172
|
-
),
|
|
1173
|
-
team_id: str | None = typer.Option(
|
|
1174
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
1175
|
-
),
|
|
1176
|
-
jira_server: str | None = typer.Option(
|
|
1177
|
-
None,
|
|
1178
|
-
"--jira-server",
|
|
1179
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
1180
|
-
),
|
|
1181
|
-
jira_email: str | None = typer.Option(
|
|
1182
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
1183
|
-
),
|
|
1184
|
-
jira_project: str | None = typer.Option(
|
|
1185
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
1186
|
-
),
|
|
1187
|
-
github_owner: str | None = typer.Option(
|
|
1188
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
1189
|
-
),
|
|
1190
|
-
github_repo: str | None = typer.Option(
|
|
1191
|
-
None, "--github-repo", help="GitHub repository name"
|
|
1192
|
-
),
|
|
1193
|
-
github_token: str | None = typer.Option(
|
|
1194
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
1195
|
-
),
|
|
1196
|
-
) -> None:
|
|
1197
|
-
"""Initialize adapter configuration only (without platform installation).
|
|
1198
|
-
|
|
1199
|
-
This command sets up adapter configuration with interactive prompts.
|
|
1200
|
-
It auto-detects adapter configuration from .env files or prompts for
|
|
1201
|
-
interactive setup if no configuration is found.
|
|
1202
|
-
|
|
1203
|
-
Creates .mcp-ticketer/config.json in the current directory.
|
|
1204
|
-
|
|
1205
|
-
RECOMMENDED: Use 'mcp-ticketer setup' instead for a complete setup
|
|
1206
|
-
experience that includes both adapter configuration and platform
|
|
1207
|
-
installation in one command.
|
|
1208
|
-
|
|
1209
|
-
The init command automatically validates your configuration after setup:
|
|
1210
|
-
- If validation passes, setup completes
|
|
1211
|
-
- If issues are detected, you can re-enter credentials, continue anyway, or exit
|
|
1212
|
-
- You get up to 3 retry attempts to fix configuration issues
|
|
1213
|
-
- You can always re-validate later with 'mcp-ticketer doctor'
|
|
1214
|
-
|
|
1215
|
-
Examples:
|
|
1216
|
-
# For first-time setup, use 'setup' instead (recommended)
|
|
1217
|
-
mcp-ticketer setup
|
|
1218
|
-
|
|
1219
|
-
# Initialize adapter only (advanced usage)
|
|
1220
|
-
mcp-ticketer init
|
|
1221
|
-
|
|
1222
|
-
# Force specific adapter
|
|
1223
|
-
mcp-ticketer init --adapter linear
|
|
1224
|
-
|
|
1225
|
-
# Initialize for different project
|
|
1226
|
-
mcp-ticketer init --path /path/to/project
|
|
1227
|
-
|
|
1228
|
-
"""
|
|
1229
|
-
from pathlib import Path
|
|
1230
|
-
|
|
1231
|
-
from ..core.env_discovery import discover_config
|
|
1232
|
-
|
|
1233
|
-
# Determine project path
|
|
1234
|
-
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
1235
|
-
|
|
1236
|
-
# Check if already initialized (unless using --global)
|
|
1237
|
-
if not global_config:
|
|
1238
|
-
config_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
1239
|
-
|
|
1240
|
-
if config_path.exists():
|
|
1241
|
-
if not typer.confirm(
|
|
1242
|
-
f"Configuration already exists at {config_path}. Overwrite?",
|
|
1243
|
-
default=False,
|
|
1244
|
-
):
|
|
1245
|
-
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
1246
|
-
raise typer.Exit(0) from None
|
|
1247
|
-
|
|
1248
|
-
# 1. Try auto-discovery if no adapter specified
|
|
1249
|
-
discovered = None
|
|
1250
|
-
adapter_type = adapter
|
|
1251
|
-
|
|
1252
|
-
if not adapter_type:
|
|
1253
|
-
console.print(
|
|
1254
|
-
"[cyan]š Auto-discovering configuration from .env files...[/cyan]"
|
|
1255
|
-
)
|
|
1256
|
-
|
|
1257
|
-
# First try our improved .env configuration loader
|
|
1258
|
-
from ..mcp.server.main import _load_env_configuration
|
|
1259
|
-
|
|
1260
|
-
env_config = _load_env_configuration()
|
|
1261
|
-
|
|
1262
|
-
if env_config:
|
|
1263
|
-
adapter_type = env_config["adapter_type"]
|
|
1264
|
-
console.print(
|
|
1265
|
-
f"[green]ā Detected {adapter_type} adapter from environment files[/green]"
|
|
1266
|
-
)
|
|
1267
|
-
|
|
1268
|
-
# Show what was discovered
|
|
1269
|
-
console.print("\n[dim]Configuration found in: .env files[/dim]")
|
|
1270
|
-
console.print("[dim]Confidence: 100%[/dim]")
|
|
1271
|
-
|
|
1272
|
-
# Ask user to confirm auto-detected adapter
|
|
1273
|
-
if not typer.confirm(
|
|
1274
|
-
f"Use detected {adapter_type} adapter?",
|
|
1275
|
-
default=True,
|
|
1276
|
-
):
|
|
1277
|
-
adapter_type = None # Will trigger interactive selection
|
|
1278
|
-
else:
|
|
1279
|
-
# Fallback to old discovery system for backward compatibility
|
|
1280
|
-
discovered = discover_config(proj_path)
|
|
1281
|
-
|
|
1282
|
-
if discovered and discovered.adapters:
|
|
1283
|
-
primary = discovered.get_primary_adapter()
|
|
1284
|
-
if primary:
|
|
1285
|
-
adapter_type = primary.adapter_type
|
|
1286
|
-
console.print(
|
|
1287
|
-
f"[green]ā Detected {adapter_type} adapter from environment files[/green]"
|
|
1288
|
-
)
|
|
1289
|
-
|
|
1290
|
-
# Show what was discovered
|
|
1291
|
-
console.print(
|
|
1292
|
-
f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
|
|
1293
|
-
)
|
|
1294
|
-
console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
|
|
1295
|
-
|
|
1296
|
-
# Ask user to confirm auto-detected adapter
|
|
1297
|
-
if not typer.confirm(
|
|
1298
|
-
f"Use detected {adapter_type} adapter?",
|
|
1299
|
-
default=True,
|
|
1300
|
-
):
|
|
1301
|
-
adapter_type = None # Will trigger interactive selection
|
|
1302
|
-
else:
|
|
1303
|
-
adapter_type = None # Will trigger interactive selection
|
|
1304
|
-
else:
|
|
1305
|
-
adapter_type = None # Will trigger interactive selection
|
|
1306
|
-
|
|
1307
|
-
# If no adapter determined, show interactive selection
|
|
1308
|
-
if not adapter_type:
|
|
1309
|
-
adapter_type = _prompt_for_adapter_selection(console)
|
|
1310
|
-
|
|
1311
|
-
# 2. Create configuration based on adapter type
|
|
1312
|
-
config = {"default_adapter": adapter_type, "adapters": {}}
|
|
1313
|
-
|
|
1314
|
-
# 3. If discovered and matches adapter_type, use discovered config
|
|
1315
|
-
if discovered and adapter_type != "aitrackdown":
|
|
1316
|
-
discovered_adapter = discovered.get_adapter_by_type(adapter_type)
|
|
1317
|
-
if discovered_adapter:
|
|
1318
|
-
adapter_config = discovered_adapter.config.copy()
|
|
1319
|
-
# Ensure the config has the correct 'type' field
|
|
1320
|
-
adapter_config["type"] = adapter_type
|
|
1321
|
-
# Remove 'adapter' field if present (legacy)
|
|
1322
|
-
adapter_config.pop("adapter", None)
|
|
1323
|
-
config["adapters"][adapter_type] = adapter_config
|
|
1324
|
-
|
|
1325
|
-
# 4. Handle manual configuration for specific adapters
|
|
1326
|
-
if adapter_type == "aitrackdown":
|
|
1327
|
-
config["adapters"]["aitrackdown"] = {
|
|
1328
|
-
"type": "aitrackdown",
|
|
1329
|
-
"base_path": base_path or ".aitrackdown",
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
elif adapter_type == "linear":
|
|
1333
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
1334
|
-
if adapter_type not in config["adapters"]:
|
|
1335
|
-
# API Key
|
|
1336
|
-
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
1337
|
-
if not linear_api_key:
|
|
1338
|
-
console.print("\n[bold]Linear Configuration[/bold]")
|
|
1339
|
-
console.print("You need a Linear API key to connect to Linear.")
|
|
1340
|
-
console.print(
|
|
1341
|
-
"[dim]Get your API key at: https://linear.app/settings/api[/dim]\n"
|
|
1342
|
-
)
|
|
1343
|
-
|
|
1344
|
-
linear_api_key = typer.prompt(
|
|
1345
|
-
"Enter your Linear API key", hide_input=True
|
|
1346
|
-
)
|
|
1347
|
-
|
|
1348
|
-
# Team ID or Team Key or Team URL
|
|
1349
|
-
# Try environment variables first
|
|
1350
|
-
linear_team_key = os.getenv("LINEAR_TEAM_KEY")
|
|
1351
|
-
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
1352
|
-
|
|
1353
|
-
if not linear_team_key and not linear_team_id:
|
|
1354
|
-
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
1355
|
-
console.print("You can provide either:")
|
|
1356
|
-
console.print(
|
|
1357
|
-
" 1. Team URL (e.g., https://linear.app/workspace/team/TEAMKEY/active)"
|
|
1358
|
-
)
|
|
1359
|
-
console.print(" 2. Team key (e.g., 'ENG', 'DESIGN', 'PRODUCT')")
|
|
1360
|
-
console.print(" 3. Team ID (UUID)")
|
|
1361
|
-
console.print(
|
|
1362
|
-
"[dim]Find team URL or key in: Linear ā Your Team ā Team Issues Page[/dim]\n"
|
|
1363
|
-
)
|
|
1364
|
-
|
|
1365
|
-
team_input = typer.prompt("Team URL, key, or ID")
|
|
1366
|
-
|
|
1367
|
-
# Check if input is a URL
|
|
1368
|
-
if team_input.startswith("https://linear.app/"):
|
|
1369
|
-
console.print("[cyan]Detected team URL, deriving team ID...[/cyan]")
|
|
1370
|
-
import asyncio
|
|
1371
|
-
|
|
1372
|
-
from .linear_commands import derive_team_from_url
|
|
1373
|
-
|
|
1374
|
-
derived_team_id, error = asyncio.run(
|
|
1375
|
-
derive_team_from_url(linear_api_key, team_input)
|
|
1376
|
-
)
|
|
1377
|
-
|
|
1378
|
-
if derived_team_id:
|
|
1379
|
-
linear_team_id = derived_team_id
|
|
1380
|
-
console.print(
|
|
1381
|
-
"[green]ā[/green] Successfully derived team ID from URL"
|
|
1382
|
-
)
|
|
1383
|
-
else:
|
|
1384
|
-
console.print(f"[red]Error:[/red] {error}")
|
|
1385
|
-
console.print("Please provide team key or ID manually instead.")
|
|
1386
|
-
team_input = typer.prompt("Team key or ID")
|
|
1387
|
-
|
|
1388
|
-
# Store as either team_key or team_id based on format
|
|
1389
|
-
if len(team_input) > 20: # Likely a UUID
|
|
1390
|
-
linear_team_id = team_input
|
|
1391
|
-
else:
|
|
1392
|
-
linear_team_key = team_input
|
|
1393
|
-
else:
|
|
1394
|
-
# Input is team key or ID
|
|
1395
|
-
if len(team_input) > 20: # Likely a UUID
|
|
1396
|
-
linear_team_id = team_input
|
|
1397
|
-
else:
|
|
1398
|
-
linear_team_key = team_input
|
|
1399
|
-
|
|
1400
|
-
# Validate required fields (following JIRA pattern)
|
|
1401
|
-
if not linear_api_key:
|
|
1402
|
-
console.print("[red]Error:[/red] Linear API key is required")
|
|
1403
|
-
raise typer.Exit(1) from None
|
|
1404
|
-
|
|
1405
|
-
if not linear_team_id and not linear_team_key:
|
|
1406
|
-
console.print(
|
|
1407
|
-
"[red]Error:[/red] Linear requires either team ID or team key"
|
|
1408
|
-
)
|
|
1409
|
-
raise typer.Exit(1) from None
|
|
1410
|
-
|
|
1411
|
-
# Build configuration
|
|
1412
|
-
linear_config = {
|
|
1413
|
-
"api_key": linear_api_key,
|
|
1414
|
-
"type": "linear",
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
# Save whichever was provided
|
|
1418
|
-
if linear_team_key:
|
|
1419
|
-
linear_config["team_key"] = linear_team_key
|
|
1420
|
-
if linear_team_id:
|
|
1421
|
-
linear_config["team_id"] = linear_team_id
|
|
1422
|
-
|
|
1423
|
-
config["adapters"]["linear"] = linear_config
|
|
1424
|
-
|
|
1425
|
-
elif adapter_type == "jira":
|
|
1426
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
1427
|
-
if adapter_type not in config["adapters"]:
|
|
1428
|
-
server = jira_server or os.getenv("JIRA_SERVER")
|
|
1429
|
-
email = jira_email or os.getenv("JIRA_EMAIL")
|
|
1430
|
-
token = api_key or os.getenv("JIRA_API_TOKEN")
|
|
1431
|
-
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
1432
|
-
|
|
1433
|
-
# Interactive prompts for missing values
|
|
1434
|
-
if not server:
|
|
1435
|
-
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
1436
|
-
console.print("Enter your JIRA server details.\n")
|
|
1437
|
-
|
|
1438
|
-
server = typer.prompt(
|
|
1439
|
-
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
1440
|
-
)
|
|
1441
|
-
|
|
1442
|
-
if not email:
|
|
1443
|
-
email = typer.prompt("Your JIRA email address")
|
|
1444
|
-
|
|
1445
|
-
if not token:
|
|
1446
|
-
console.print("\nYou need a JIRA API token.")
|
|
1447
|
-
console.print(
|
|
1448
|
-
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
1449
|
-
)
|
|
1450
|
-
|
|
1451
|
-
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
1452
|
-
|
|
1453
|
-
if not project:
|
|
1454
|
-
project = typer.prompt(
|
|
1455
|
-
"Default JIRA project key (optional, press Enter to skip)",
|
|
1456
|
-
default="",
|
|
1457
|
-
show_default=False,
|
|
1458
|
-
)
|
|
1459
|
-
|
|
1460
|
-
# Validate required fields
|
|
1461
|
-
if not server:
|
|
1462
|
-
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
1463
|
-
raise typer.Exit(1) from None
|
|
1464
|
-
|
|
1465
|
-
if not email:
|
|
1466
|
-
console.print("[red]Error:[/red] JIRA email is required")
|
|
1467
|
-
raise typer.Exit(1) from None
|
|
1468
|
-
|
|
1469
|
-
if not token:
|
|
1470
|
-
console.print("[red]Error:[/red] JIRA API token is required")
|
|
1471
|
-
raise typer.Exit(1) from None
|
|
1472
|
-
|
|
1473
|
-
jira_config = {
|
|
1474
|
-
"server": server,
|
|
1475
|
-
"email": email,
|
|
1476
|
-
"api_token": token,
|
|
1477
|
-
"type": "jira",
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
if project:
|
|
1481
|
-
jira_config["project_key"] = project
|
|
1482
|
-
|
|
1483
|
-
config["adapters"]["jira"] = jira_config
|
|
1484
|
-
|
|
1485
|
-
elif adapter_type == "github":
|
|
1486
|
-
# If not auto-discovered, build from CLI params or prompt
|
|
1487
|
-
if adapter_type not in config["adapters"]:
|
|
1488
|
-
owner = github_owner or os.getenv("GITHUB_OWNER")
|
|
1489
|
-
repo = github_repo or os.getenv("GITHUB_REPO")
|
|
1490
|
-
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
1491
|
-
|
|
1492
|
-
# Interactive prompts for missing values
|
|
1493
|
-
if not owner:
|
|
1494
|
-
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
1495
|
-
console.print("Enter your GitHub repository details.\n")
|
|
1496
|
-
|
|
1497
|
-
owner = typer.prompt(
|
|
1498
|
-
"GitHub repository owner (username or organization)"
|
|
1499
|
-
)
|
|
1500
|
-
|
|
1501
|
-
if not repo:
|
|
1502
|
-
repo = typer.prompt("GitHub repository name")
|
|
1503
|
-
|
|
1504
|
-
if not token:
|
|
1505
|
-
console.print("\nYou need a GitHub Personal Access Token.")
|
|
1506
|
-
console.print(
|
|
1507
|
-
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
1508
|
-
)
|
|
1509
|
-
console.print(
|
|
1510
|
-
"[dim]Required scopes: repo (for private repos) or public_repo (for public repos)[/dim]\n"
|
|
1511
|
-
)
|
|
1512
|
-
|
|
1513
|
-
token = typer.prompt(
|
|
1514
|
-
"Enter your GitHub Personal Access Token", hide_input=True
|
|
1515
|
-
)
|
|
1516
|
-
|
|
1517
|
-
# Validate required fields
|
|
1518
|
-
if not owner:
|
|
1519
|
-
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
1520
|
-
raise typer.Exit(1) from None
|
|
1521
|
-
|
|
1522
|
-
if not repo:
|
|
1523
|
-
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
1524
|
-
raise typer.Exit(1) from None
|
|
1525
|
-
|
|
1526
|
-
if not token:
|
|
1527
|
-
console.print(
|
|
1528
|
-
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
1529
|
-
)
|
|
1530
|
-
raise typer.Exit(1) from None
|
|
1531
|
-
|
|
1532
|
-
config["adapters"]["github"] = {
|
|
1533
|
-
"owner": owner,
|
|
1534
|
-
"repo": repo,
|
|
1535
|
-
"token": token,
|
|
1536
|
-
"type": "github",
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
# 5. Save to project-local config (global config deprecated for security)
|
|
1540
|
-
# Always save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
|
|
1541
|
-
config_file_path = proj_path / ".mcp-ticketer" / "config.json"
|
|
1542
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1543
|
-
|
|
1544
|
-
with open(config_file_path, "w") as f:
|
|
1545
|
-
json.dump(config, f, indent=2)
|
|
1546
|
-
|
|
1547
|
-
if global_config:
|
|
1548
|
-
console.print(
|
|
1549
|
-
"[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
|
|
1550
|
-
)
|
|
1551
|
-
|
|
1552
|
-
console.print(f"[green]ā Initialized with {adapter_type} adapter[/green]")
|
|
1553
|
-
console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
|
|
1554
|
-
|
|
1555
|
-
# Add .mcp-ticketer to .gitignore if not already there
|
|
1556
|
-
gitignore_path = proj_path / ".gitignore"
|
|
1557
|
-
if gitignore_path.exists():
|
|
1558
|
-
gitignore_content = gitignore_path.read_text()
|
|
1559
|
-
if ".mcp-ticketer" not in gitignore_content:
|
|
1560
|
-
with open(gitignore_path, "a") as f:
|
|
1561
|
-
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
1562
|
-
console.print("[dim]ā Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
1563
|
-
else:
|
|
1564
|
-
# Create .gitignore if it doesn't exist
|
|
1565
|
-
with open(gitignore_path, "w") as f:
|
|
1566
|
-
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
1567
|
-
console.print("[dim]ā Created .gitignore with .mcp-ticketer/[/dim]")
|
|
1568
|
-
|
|
1569
|
-
# Validate configuration with loop for corrections
|
|
1570
|
-
import asyncio
|
|
1571
|
-
|
|
1572
|
-
if not asyncio.run(
|
|
1573
|
-
_validate_configuration_with_retry(
|
|
1574
|
-
console, adapter_type, config_file_path, proj_path
|
|
1575
|
-
)
|
|
1576
|
-
):
|
|
1577
|
-
# User chose to exit without valid configuration
|
|
1578
|
-
raise typer.Exit(1) from None
|
|
1579
|
-
|
|
1580
|
-
# Show next steps
|
|
1581
|
-
_show_next_steps(console, adapter_type, config_file_path)
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
def _show_next_steps(
|
|
1585
|
-
console: Console, adapter_type: str, config_file_path: Path
|
|
1586
|
-
) -> None:
|
|
1587
|
-
"""Show helpful next steps after initialization.
|
|
1588
|
-
|
|
1589
|
-
Args:
|
|
1590
|
-
console: Rich console for output
|
|
1591
|
-
adapter_type: Type of adapter that was configured
|
|
1592
|
-
config_file_path: Path to the configuration file
|
|
1593
|
-
|
|
1594
|
-
"""
|
|
1595
|
-
console.print("\n[bold green]š Setup Complete![/bold green]")
|
|
1596
|
-
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
1597
|
-
|
|
1598
|
-
console.print("[bold]Next Steps:[/bold]")
|
|
1599
|
-
console.print("1. [cyan]Create a test ticket:[/cyan]")
|
|
1600
|
-
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
1601
|
-
|
|
1602
|
-
if adapter_type != "aitrackdown":
|
|
1603
|
-
console.print(
|
|
1604
|
-
f"\n2. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
|
|
1605
|
-
)
|
|
1606
|
-
if adapter_type == "linear":
|
|
1607
|
-
console.print(" Check your Linear workspace for the new ticket")
|
|
1608
|
-
elif adapter_type == "github":
|
|
1609
|
-
console.print(" Check your GitHub repository's Issues tab")
|
|
1610
|
-
elif adapter_type == "jira":
|
|
1611
|
-
console.print(" Check your JIRA project for the new ticket")
|
|
1612
|
-
else:
|
|
1613
|
-
console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
|
|
1614
|
-
console.print(" ls .aitrackdown/")
|
|
1615
|
-
|
|
1616
|
-
console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
|
|
1617
|
-
console.print(" mcp-ticketer install claude-code # For Claude Code")
|
|
1618
|
-
console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
|
|
1619
|
-
console.print(" mcp-ticketer install auggie # For Auggie")
|
|
1620
|
-
console.print(" mcp-ticketer install gemini # For Gemini CLI")
|
|
1621
|
-
|
|
1622
|
-
console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
|
|
1623
|
-
console.print(
|
|
1624
|
-
"[dim]Run 'mcp-ticketer doctor' to re-validate configuration anytime[/dim]"
|
|
1625
|
-
)
|
|
1626
|
-
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
@app.command("set")
|
|
1630
|
-
def set_config(
|
|
1631
|
-
adapter: AdapterType | None = typer.Option(
|
|
1632
|
-
None, "--adapter", "-a", help="Set default adapter"
|
|
1633
|
-
),
|
|
1634
|
-
team_key: str | None = typer.Option(
|
|
1635
|
-
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
1636
|
-
),
|
|
1637
|
-
team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
1638
|
-
owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
|
|
1639
|
-
repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
1640
|
-
server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
|
|
1641
|
-
project: str | None = typer.Option(None, "--project", help="JIRA project key"),
|
|
1642
|
-
base_path: str | None = typer.Option(
|
|
1643
|
-
None, "--base-path", help="AITrackdown base path"
|
|
1644
|
-
),
|
|
1645
|
-
) -> None:
|
|
1646
|
-
"""Set default adapter and adapter-specific configuration.
|
|
1647
|
-
|
|
1648
|
-
When called without arguments, shows current configuration.
|
|
1649
|
-
"""
|
|
1650
|
-
if not any([adapter, team_key, team_id, owner, repo, server, project, base_path]):
|
|
1651
|
-
# Show current configuration
|
|
1652
|
-
config = load_config()
|
|
1653
|
-
console.print("[bold]Current Configuration:[/bold]")
|
|
1654
|
-
console.print(
|
|
1655
|
-
f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
|
|
1656
|
-
)
|
|
1657
|
-
|
|
1658
|
-
adapters_config = config.get("adapters", {})
|
|
1659
|
-
if adapters_config:
|
|
1660
|
-
console.print("\n[bold]Adapter Settings:[/bold]")
|
|
1661
|
-
for adapter_name, adapter_config in adapters_config.items():
|
|
1662
|
-
console.print(f"\n[cyan]{adapter_name}:[/cyan]")
|
|
1663
|
-
for key, value in adapter_config.items():
|
|
1664
|
-
# Don't display sensitive values like tokens
|
|
1665
|
-
if (
|
|
1666
|
-
"token" in key.lower()
|
|
1667
|
-
or "key" in key.lower()
|
|
1668
|
-
and "team" not in key.lower()
|
|
1669
|
-
):
|
|
1670
|
-
value = "***" if value else "not set"
|
|
1671
|
-
console.print(f" {key}: {value}")
|
|
1672
|
-
return
|
|
1673
|
-
|
|
1674
|
-
updates = {}
|
|
1675
|
-
|
|
1676
|
-
# Set default adapter
|
|
1677
|
-
if adapter:
|
|
1678
|
-
updates["default_adapter"] = adapter.value
|
|
1679
|
-
console.print(f"[green]ā[/green] Default adapter set to: {adapter.value}")
|
|
1680
|
-
|
|
1681
|
-
# Build adapter-specific configuration
|
|
1682
|
-
adapter_configs = {}
|
|
1683
|
-
|
|
1684
|
-
# Linear configuration
|
|
1685
|
-
if team_key or team_id:
|
|
1686
|
-
linear_config = {}
|
|
1687
|
-
if team_key:
|
|
1688
|
-
linear_config["team_key"] = team_key
|
|
1689
|
-
if team_id:
|
|
1690
|
-
linear_config["team_id"] = team_id
|
|
1691
|
-
adapter_configs["linear"] = linear_config
|
|
1692
|
-
console.print("[green]ā[/green] Linear settings updated")
|
|
1693
|
-
|
|
1694
|
-
# GitHub configuration
|
|
1695
|
-
if owner or repo:
|
|
1696
|
-
github_config = {}
|
|
1697
|
-
if owner:
|
|
1698
|
-
github_config["owner"] = owner
|
|
1699
|
-
if repo:
|
|
1700
|
-
github_config["repo"] = repo
|
|
1701
|
-
adapter_configs["github"] = github_config
|
|
1702
|
-
console.print("[green]ā[/green] GitHub settings updated")
|
|
1703
|
-
|
|
1704
|
-
# JIRA configuration
|
|
1705
|
-
if server or project:
|
|
1706
|
-
jira_config = {}
|
|
1707
|
-
if server:
|
|
1708
|
-
jira_config["server"] = server
|
|
1709
|
-
if project:
|
|
1710
|
-
jira_config["project_key"] = project
|
|
1711
|
-
adapter_configs["jira"] = jira_config
|
|
1712
|
-
console.print("[green]ā[/green] JIRA settings updated")
|
|
1713
|
-
|
|
1714
|
-
# AITrackdown configuration
|
|
1715
|
-
if base_path:
|
|
1716
|
-
adapter_configs["aitrackdown"] = {"base_path": base_path}
|
|
1717
|
-
console.print("[green]ā[/green] AITrackdown settings updated")
|
|
1718
|
-
|
|
1719
|
-
if adapter_configs:
|
|
1720
|
-
updates["adapters"] = adapter_configs
|
|
1721
|
-
|
|
1722
|
-
# Merge and save configuration
|
|
1723
|
-
if updates:
|
|
1724
|
-
config = merge_config(updates)
|
|
1725
|
-
save_config(config)
|
|
1726
|
-
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
@app.command("configure")
|
|
1730
|
-
def configure_command(
|
|
1731
|
-
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
1732
|
-
adapter: str | None = typer.Option(
|
|
1733
|
-
None, "--adapter", help="Set default adapter type"
|
|
1734
|
-
),
|
|
1735
|
-
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
1736
|
-
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
1737
|
-
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
1738
|
-
global_scope: bool = typer.Option(
|
|
1739
|
-
False,
|
|
1740
|
-
"--global",
|
|
1741
|
-
"-g",
|
|
1742
|
-
help="Save to global config instead of project-specific",
|
|
1743
|
-
),
|
|
1744
|
-
) -> None:
|
|
1745
|
-
"""Configure MCP Ticketer integration.
|
|
1746
|
-
|
|
1747
|
-
Run without arguments to launch interactive wizard.
|
|
1748
|
-
Use --show to display current configuration.
|
|
1749
|
-
Use options to set specific values directly.
|
|
1750
|
-
"""
|
|
1751
|
-
# Show configuration
|
|
1752
|
-
if show:
|
|
1753
|
-
show_current_config()
|
|
1754
|
-
return
|
|
1755
|
-
|
|
1756
|
-
# Direct configuration
|
|
1757
|
-
if any([adapter, api_key, project_id, team_id]):
|
|
1758
|
-
set_adapter_config(
|
|
1759
|
-
adapter=adapter,
|
|
1760
|
-
api_key=api_key,
|
|
1761
|
-
project_id=project_id,
|
|
1762
|
-
team_id=team_id,
|
|
1763
|
-
global_scope=global_scope,
|
|
1764
|
-
)
|
|
1765
|
-
return
|
|
1766
|
-
|
|
1767
|
-
# Run interactive wizard
|
|
1768
|
-
configure_wizard()
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
@app.command("migrate-config")
|
|
1772
|
-
def migrate_config(
|
|
1773
|
-
dry_run: bool = typer.Option(
|
|
1774
|
-
False, "--dry-run", help="Show what would be done without making changes"
|
|
1775
|
-
),
|
|
1776
|
-
) -> None:
|
|
1777
|
-
"""Migrate configuration from old format to new format.
|
|
1778
|
-
|
|
1779
|
-
This command will:
|
|
1780
|
-
1. Detect old configuration format
|
|
1781
|
-
2. Convert to new schema
|
|
1782
|
-
3. Backup old config
|
|
1783
|
-
4. Apply new config
|
|
1784
|
-
"""
|
|
1785
|
-
migrate_config_command(dry_run=dry_run)
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
@app.command("queue-status", deprecated=True, hidden=True)
|
|
1789
|
-
def old_queue_status_command() -> None:
|
|
1790
|
-
"""Show queue and worker status.
|
|
1791
|
-
|
|
1792
|
-
DEPRECATED: Use 'mcp-ticketer queue status' instead.
|
|
1793
|
-
"""
|
|
1794
|
-
console.print(
|
|
1795
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer queue status' instead.[/yellow]\n"
|
|
1796
|
-
)
|
|
1797
|
-
|
|
1798
|
-
queue = Queue()
|
|
1799
|
-
manager = WorkerManager()
|
|
1800
|
-
|
|
1801
|
-
# Get queue stats
|
|
1802
|
-
stats = queue.get_stats()
|
|
1803
|
-
pending = stats.get(QueueStatus.PENDING.value, 0)
|
|
1804
|
-
|
|
1805
|
-
# Show queue status
|
|
1806
|
-
console.print("[bold]Queue Status:[/bold]")
|
|
1807
|
-
console.print(f" Pending: {pending}")
|
|
1808
|
-
console.print(f" Processing: {stats.get(QueueStatus.PROCESSING.value, 0)}")
|
|
1809
|
-
console.print(f" Completed: {stats.get(QueueStatus.COMPLETED.value, 0)}")
|
|
1810
|
-
console.print(f" Failed: {stats.get(QueueStatus.FAILED.value, 0)}")
|
|
1811
|
-
|
|
1812
|
-
# Show worker status
|
|
1813
|
-
worker_status = manager.get_status()
|
|
1814
|
-
if worker_status["running"]:
|
|
1815
|
-
console.print(
|
|
1816
|
-
f"\n[green]ā Worker is running[/green] (PID: {worker_status.get('pid')})"
|
|
1817
|
-
)
|
|
1818
|
-
else:
|
|
1819
|
-
console.print("\n[red]ā Worker is not running[/red]")
|
|
1820
|
-
if pending > 0:
|
|
1821
|
-
console.print(
|
|
1822
|
-
"[yellow]Note: There are pending items. Start worker with 'mcp-ticketer queue worker start'[/yellow]"
|
|
1823
|
-
)
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
@app.command("queue-health", deprecated=True, hidden=True)
|
|
1827
|
-
def old_queue_health_command(
|
|
1828
|
-
auto_repair: bool = typer.Option(
|
|
1829
|
-
False, "--auto-repair", help="Attempt automatic repair of issues"
|
|
1830
|
-
),
|
|
1831
|
-
verbose: bool = typer.Option(
|
|
1832
|
-
False, "--verbose", "-v", help="Show detailed health information"
|
|
1833
|
-
),
|
|
1834
|
-
) -> None:
|
|
1835
|
-
"""Check queue system health and detect issues immediately.
|
|
1836
|
-
|
|
1837
|
-
DEPRECATED: Use 'mcp-ticketer queue health' instead.
|
|
1838
|
-
"""
|
|
1839
|
-
console.print(
|
|
1840
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer queue health' instead.[/yellow]\n"
|
|
1841
|
-
)
|
|
1842
|
-
health_monitor = QueueHealthMonitor()
|
|
1843
|
-
health = health_monitor.check_health()
|
|
1844
|
-
|
|
1845
|
-
# Display overall status
|
|
1846
|
-
status_color = {
|
|
1847
|
-
HealthStatus.HEALTHY: "green",
|
|
1848
|
-
HealthStatus.WARNING: "yellow",
|
|
1849
|
-
HealthStatus.CRITICAL: "red",
|
|
1850
|
-
HealthStatus.FAILED: "red",
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
status_icon = {
|
|
1854
|
-
HealthStatus.HEALTHY: "ā",
|
|
1855
|
-
HealthStatus.WARNING: "ā ļø",
|
|
1856
|
-
HealthStatus.CRITICAL: "šØ",
|
|
1857
|
-
HealthStatus.FAILED: "ā",
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
color = status_color.get(health["status"], "white")
|
|
1861
|
-
icon = status_icon.get(health["status"], "?")
|
|
1862
|
-
|
|
1863
|
-
console.print(f"[{color}]{icon} Queue Health: {health['status'].upper()}[/{color}]")
|
|
1864
|
-
console.print(f"Last checked: {health['timestamp']}")
|
|
1865
|
-
|
|
1866
|
-
# Display alerts
|
|
1867
|
-
if health["alerts"]:
|
|
1868
|
-
console.print("\n[bold]Issues Found:[/bold]")
|
|
1869
|
-
for alert in health["alerts"]:
|
|
1870
|
-
alert_color = status_color.get(alert["level"], "white")
|
|
1871
|
-
console.print(f"[{alert_color}] ⢠{alert['message']}[/{alert_color}]")
|
|
1872
|
-
|
|
1873
|
-
if verbose and alert.get("details"):
|
|
1874
|
-
for key, value in alert["details"].items():
|
|
1875
|
-
console.print(f" {key}: {value}")
|
|
1876
|
-
else:
|
|
1877
|
-
console.print("\n[green]ā No issues detected[/green]")
|
|
1878
|
-
|
|
1879
|
-
# Auto-repair if requested
|
|
1880
|
-
if auto_repair and health["status"] in [
|
|
1881
|
-
HealthStatus.CRITICAL,
|
|
1882
|
-
HealthStatus.WARNING,
|
|
1883
|
-
]:
|
|
1884
|
-
console.print("\n[yellow]Attempting automatic repair...[/yellow]")
|
|
1885
|
-
repair_result = health_monitor.auto_repair()
|
|
1886
|
-
|
|
1887
|
-
if repair_result["actions_taken"]:
|
|
1888
|
-
console.print("[green]Repair actions taken:[/green]")
|
|
1889
|
-
for action in repair_result["actions_taken"]:
|
|
1890
|
-
console.print(f"[green] ā {action}[/green]")
|
|
1891
|
-
|
|
1892
|
-
# Re-check health
|
|
1893
|
-
console.print("\n[yellow]Re-checking health after repair...[/yellow]")
|
|
1894
|
-
new_health = health_monitor.check_health()
|
|
1895
|
-
new_color = status_color.get(new_health["status"], "white")
|
|
1896
|
-
new_icon = status_icon.get(new_health["status"], "?")
|
|
1897
|
-
console.print(
|
|
1898
|
-
f"[{new_color}]{new_icon} Updated Health: {new_health['status'].upper()}[/{new_color}]"
|
|
1899
|
-
)
|
|
1900
|
-
else:
|
|
1901
|
-
console.print("[yellow]No repair actions available[/yellow]")
|
|
1902
|
-
|
|
1903
|
-
# Exit with appropriate code
|
|
1904
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1905
|
-
raise typer.Exit(1) from None
|
|
1906
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
1907
|
-
raise typer.Exit(2) from None
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
@app.command(deprecated=True, hidden=True)
|
|
1911
|
-
def create(
|
|
1912
|
-
title: str = typer.Argument(..., help="Ticket title"),
|
|
1913
|
-
description: str | None = typer.Option(
|
|
1914
|
-
None, "--description", "-d", help="Ticket description"
|
|
1915
|
-
),
|
|
1916
|
-
priority: Priority = typer.Option(
|
|
1917
|
-
Priority.MEDIUM, "--priority", "-p", help="Priority level"
|
|
1918
|
-
),
|
|
1919
|
-
tags: list[str] | None = typer.Option(
|
|
1920
|
-
None, "--tag", "-t", help="Tags (can be specified multiple times)"
|
|
1921
|
-
),
|
|
1922
|
-
assignee: str | None = typer.Option(
|
|
1923
|
-
None, "--assignee", "-a", help="Assignee username"
|
|
1924
|
-
),
|
|
1925
|
-
project: str | None = typer.Option(
|
|
1926
|
-
None,
|
|
1927
|
-
"--project",
|
|
1928
|
-
help="Parent project/epic ID (synonym for --epic)",
|
|
1929
|
-
),
|
|
1930
|
-
epic: str | None = typer.Option(
|
|
1931
|
-
None,
|
|
1932
|
-
"--epic",
|
|
1933
|
-
help="Parent epic/project ID (synonym for --project)",
|
|
1934
|
-
),
|
|
1935
|
-
adapter: AdapterType | None = typer.Option(
|
|
1936
|
-
None, "--adapter", help="Override default adapter"
|
|
1937
|
-
),
|
|
1938
|
-
) -> None:
|
|
1939
|
-
"""Create a new ticket with comprehensive health checks.
|
|
1940
|
-
|
|
1941
|
-
DEPRECATED: Use 'mcp-ticketer ticket create' instead.
|
|
1942
|
-
"""
|
|
1943
|
-
console.print(
|
|
1944
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket create' instead.[/yellow]\n"
|
|
1945
|
-
)
|
|
1946
|
-
|
|
1947
|
-
# IMMEDIATE HEALTH CHECK - Critical for reliability
|
|
1948
|
-
health_monitor = QueueHealthMonitor()
|
|
1949
|
-
health = health_monitor.check_health()
|
|
1950
|
-
|
|
1951
|
-
# Display health status
|
|
1952
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1953
|
-
console.print("[red]šØ CRITICAL: Queue system has serious issues![/red]")
|
|
1954
|
-
for alert in health["alerts"]:
|
|
1955
|
-
if alert["level"] == "critical":
|
|
1956
|
-
console.print(f"[red] ⢠{alert['message']}[/red]")
|
|
1957
|
-
|
|
1958
|
-
# Attempt auto-repair
|
|
1959
|
-
console.print("[yellow]Attempting automatic repair...[/yellow]")
|
|
1960
|
-
repair_result = health_monitor.auto_repair()
|
|
1961
|
-
|
|
1962
|
-
if repair_result["actions_taken"]:
|
|
1963
|
-
for action in repair_result["actions_taken"]:
|
|
1964
|
-
console.print(f"[yellow] ā {action}[/yellow]")
|
|
1965
|
-
|
|
1966
|
-
# Re-check health after repair
|
|
1967
|
-
health = health_monitor.check_health()
|
|
1968
|
-
if health["status"] == HealthStatus.CRITICAL:
|
|
1969
|
-
console.print(
|
|
1970
|
-
"[red]ā Auto-repair failed. Manual intervention required.[/red]"
|
|
1971
|
-
)
|
|
1972
|
-
console.print(
|
|
1973
|
-
"[red]Cannot safely create ticket. Please check system status.[/red]"
|
|
1974
|
-
)
|
|
1975
|
-
raise typer.Exit(1) from None
|
|
1976
|
-
else:
|
|
1977
|
-
console.print(
|
|
1978
|
-
"[green]ā Auto-repair successful. Proceeding with ticket creation.[/green]"
|
|
1979
|
-
)
|
|
1980
|
-
else:
|
|
1981
|
-
console.print(
|
|
1982
|
-
"[red]ā No repair actions available. Manual intervention required.[/red]"
|
|
1983
|
-
)
|
|
1984
|
-
raise typer.Exit(1) from None
|
|
1985
|
-
|
|
1986
|
-
elif health["status"] == HealthStatus.WARNING:
|
|
1987
|
-
console.print("[yellow]ā ļø Warning: Queue system has minor issues[/yellow]")
|
|
1988
|
-
for alert in health["alerts"]:
|
|
1989
|
-
if alert["level"] == "warning":
|
|
1990
|
-
console.print(f"[yellow] ⢠{alert['message']}[/yellow]")
|
|
1991
|
-
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
1992
|
-
|
|
1993
|
-
# Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
|
|
1994
|
-
if adapter:
|
|
1995
|
-
# Priority 1: Command-line argument - save to config for future use
|
|
1996
|
-
adapter_name = adapter.value
|
|
1997
|
-
_save_adapter_to_config(adapter_name)
|
|
1998
|
-
else:
|
|
1999
|
-
# Priority 2: Check existing config
|
|
2000
|
-
config = load_config()
|
|
2001
|
-
adapter_name = config.get("default_adapter")
|
|
2002
|
-
|
|
2003
|
-
if not adapter_name or adapter_name == "aitrackdown":
|
|
2004
|
-
# Priority 3: Check .env files and save if found
|
|
2005
|
-
env_adapter = _discover_from_env_files()
|
|
2006
|
-
if env_adapter:
|
|
2007
|
-
adapter_name = env_adapter
|
|
2008
|
-
_save_adapter_to_config(adapter_name)
|
|
2009
|
-
else:
|
|
2010
|
-
# Priority 4: Default
|
|
2011
|
-
adapter_name = "aitrackdown"
|
|
2012
|
-
|
|
2013
|
-
# Resolve project/epic synonym - prefer whichever is provided
|
|
2014
|
-
parent_epic_id = project or epic
|
|
2015
|
-
|
|
2016
|
-
# Create task data
|
|
2017
|
-
# Import Priority for type checking
|
|
2018
|
-
from ..core.models import Priority as PriorityEnum
|
|
2019
|
-
|
|
2020
|
-
task_data = {
|
|
2021
|
-
"title": title,
|
|
2022
|
-
"description": description,
|
|
2023
|
-
"priority": priority.value if isinstance(priority, PriorityEnum) else priority,
|
|
2024
|
-
"tags": tags or [],
|
|
2025
|
-
"assignee": assignee,
|
|
2026
|
-
"parent_epic": parent_epic_id,
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
2030
|
-
if adapter_name == "linear":
|
|
2031
|
-
console.print(
|
|
2032
|
-
"[yellow]ā ļø[/yellow] Using direct operation for Linear adapter (bypassing queue)"
|
|
2033
|
-
)
|
|
2034
|
-
try:
|
|
2035
|
-
# Load configuration and create adapter directly
|
|
2036
|
-
config = load_config()
|
|
2037
|
-
adapter_config = config.get("adapters", {}).get(adapter_name, {})
|
|
2038
|
-
|
|
2039
|
-
# Import and create adapter
|
|
2040
|
-
from ..core.registry import AdapterRegistry
|
|
2041
|
-
|
|
2042
|
-
adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
|
|
2043
|
-
|
|
2044
|
-
# Create task directly
|
|
2045
|
-
from ..core.models import Priority, Task
|
|
2046
|
-
|
|
2047
|
-
task = Task(
|
|
2048
|
-
title=task_data["title"],
|
|
2049
|
-
description=task_data.get("description"),
|
|
2050
|
-
priority=(
|
|
2051
|
-
Priority(task_data["priority"])
|
|
2052
|
-
if task_data.get("priority")
|
|
2053
|
-
else Priority.MEDIUM
|
|
2054
|
-
),
|
|
2055
|
-
tags=task_data.get("tags", []),
|
|
2056
|
-
assignee=task_data.get("assignee"),
|
|
2057
|
-
parent_epic=task_data.get("parent_epic"),
|
|
2058
|
-
)
|
|
2059
|
-
|
|
2060
|
-
# Create ticket synchronously
|
|
2061
|
-
import asyncio
|
|
2062
|
-
|
|
2063
|
-
result = asyncio.run(adapter.create(task))
|
|
2064
|
-
|
|
2065
|
-
console.print(f"[green]ā[/green] Ticket created successfully: {result.id}")
|
|
2066
|
-
console.print(f" Title: {result.title}")
|
|
2067
|
-
console.print(f" Priority: {result.priority}")
|
|
2068
|
-
console.print(f" State: {result.state}")
|
|
2069
|
-
# Get URL from metadata if available
|
|
2070
|
-
if (
|
|
2071
|
-
result.metadata
|
|
2072
|
-
and "linear" in result.metadata
|
|
2073
|
-
and "url" in result.metadata["linear"]
|
|
2074
|
-
):
|
|
2075
|
-
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
2076
|
-
|
|
2077
|
-
return result.id
|
|
2078
|
-
|
|
2079
|
-
except Exception as e:
|
|
2080
|
-
console.print(f"[red]ā[/red] Failed to create ticket: {e}")
|
|
2081
|
-
raise
|
|
2082
|
-
|
|
2083
|
-
# Use queue for other adapters
|
|
2084
|
-
queue = Queue()
|
|
2085
|
-
queue_id = queue.add(
|
|
2086
|
-
ticket_data=task_data,
|
|
2087
|
-
adapter=adapter_name,
|
|
2088
|
-
operation="create",
|
|
2089
|
-
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
2090
|
-
)
|
|
2091
|
-
|
|
2092
|
-
# Register in ticket registry for tracking
|
|
2093
|
-
registry = TicketRegistry()
|
|
2094
|
-
registry.register_ticket_operation(
|
|
2095
|
-
queue_id, adapter_name, "create", title, task_data
|
|
2096
|
-
)
|
|
2097
|
-
|
|
2098
|
-
console.print(f"[green]ā[/green] Queued ticket creation: {queue_id}")
|
|
2099
|
-
console.print(f" Title: {title}")
|
|
2100
|
-
console.print(f" Priority: {priority}")
|
|
2101
|
-
console.print(f" Adapter: {adapter_name}")
|
|
2102
|
-
console.print("[dim]Use 'mcp-ticketer check {queue_id}' to check progress[/dim]")
|
|
2103
|
-
|
|
2104
|
-
# Start worker if needed with immediate feedback
|
|
2105
|
-
manager = WorkerManager()
|
|
2106
|
-
worker_started = manager.start_if_needed()
|
|
2107
|
-
|
|
2108
|
-
if worker_started:
|
|
2109
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
2110
|
-
|
|
2111
|
-
# Give immediate feedback on processing
|
|
2112
|
-
import time
|
|
2113
|
-
|
|
2114
|
-
time.sleep(1) # Brief pause to let worker start
|
|
2115
|
-
|
|
2116
|
-
# Check if item is being processed
|
|
2117
|
-
item = queue.get_item(queue_id)
|
|
2118
|
-
if item and item.status == QueueStatus.PROCESSING:
|
|
2119
|
-
console.print("[green]ā Item is being processed by worker[/green]")
|
|
2120
|
-
elif item and item.status == QueueStatus.PENDING:
|
|
2121
|
-
console.print("[yellow]ā³ Item is queued for processing[/yellow]")
|
|
2122
|
-
else:
|
|
2123
|
-
console.print(
|
|
2124
|
-
"[red]ā ļø Item status unclear - check with 'mcp-ticketer check {queue_id}'[/red]"
|
|
2125
|
-
)
|
|
2126
|
-
else:
|
|
2127
|
-
# Worker didn't start - this is a problem
|
|
2128
|
-
pending_count = queue.get_pending_count()
|
|
2129
|
-
if pending_count > 1: # More than just this item
|
|
2130
|
-
console.print(
|
|
2131
|
-
f"[red]ā Worker failed to start with {pending_count} pending items![/red]"
|
|
2132
|
-
)
|
|
2133
|
-
console.print(
|
|
2134
|
-
"[red]This is a critical issue. Try 'mcp-ticketer queue worker start' manually.[/red]"
|
|
2135
|
-
)
|
|
2136
|
-
else:
|
|
2137
|
-
console.print(
|
|
2138
|
-
"[yellow]Worker not started (no other pending items)[/yellow]"
|
|
2139
|
-
)
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
@app.command("list", deprecated=True, hidden=True)
|
|
2143
|
-
def list_tickets(
|
|
2144
|
-
state: TicketState | None = typer.Option(
|
|
2145
|
-
None, "--state", "-s", help="Filter by state"
|
|
2146
|
-
),
|
|
2147
|
-
priority: Priority | None = typer.Option(
|
|
2148
|
-
None, "--priority", "-p", help="Filter by priority"
|
|
2149
|
-
),
|
|
2150
|
-
limit: int = typer.Option(10, "--limit", "-l", help="Maximum number of tickets"),
|
|
2151
|
-
adapter: AdapterType | None = typer.Option(
|
|
2152
|
-
None, "--adapter", help="Override default adapter"
|
|
2153
|
-
),
|
|
2154
|
-
) -> None:
|
|
2155
|
-
"""List tickets with optional filters.
|
|
2156
|
-
|
|
2157
|
-
DEPRECATED: Use 'mcp-ticketer ticket list' instead.
|
|
2158
|
-
"""
|
|
2159
|
-
console.print(
|
|
2160
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
|
|
2161
|
-
)
|
|
2162
|
-
|
|
2163
|
-
async def _list() -> None:
|
|
2164
|
-
adapter_instance = get_adapter(
|
|
2165
|
-
override_adapter=adapter.value if adapter else None
|
|
2166
|
-
)
|
|
2167
|
-
filters = {}
|
|
2168
|
-
if state:
|
|
2169
|
-
filters["state"] = state
|
|
2170
|
-
if priority:
|
|
2171
|
-
filters["priority"] = priority
|
|
2172
|
-
return await adapter_instance.list(limit=limit, filters=filters)
|
|
2173
|
-
|
|
2174
|
-
tickets = asyncio.run(_list())
|
|
2175
|
-
|
|
2176
|
-
if not tickets:
|
|
2177
|
-
console.print("[yellow]No tickets found[/yellow]")
|
|
2178
|
-
return
|
|
2179
|
-
|
|
2180
|
-
# Create table
|
|
2181
|
-
table = Table(title="Tickets")
|
|
2182
|
-
table.add_column("ID", style="cyan", no_wrap=True)
|
|
2183
|
-
table.add_column("Title", style="white")
|
|
2184
|
-
table.add_column("State", style="green")
|
|
2185
|
-
table.add_column("Priority", style="yellow")
|
|
2186
|
-
table.add_column("Assignee", style="blue")
|
|
2187
|
-
|
|
2188
|
-
for ticket in tickets:
|
|
2189
|
-
# Handle assignee field - Epic doesn't have assignee, Task does
|
|
2190
|
-
assignee = getattr(ticket, "assignee", None) or "-"
|
|
2191
|
-
|
|
2192
|
-
table.add_row(
|
|
2193
|
-
ticket.id or "N/A",
|
|
2194
|
-
ticket.title,
|
|
2195
|
-
ticket.state,
|
|
2196
|
-
ticket.priority,
|
|
2197
|
-
assignee,
|
|
2198
|
-
)
|
|
2199
|
-
|
|
2200
|
-
console.print(table)
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
@app.command(deprecated=True, hidden=True)
|
|
2204
|
-
def show(
|
|
2205
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2206
|
-
comments: bool = typer.Option(False, "--comments", "-c", help="Show comments"),
|
|
2207
|
-
adapter: AdapterType | None = typer.Option(
|
|
2208
|
-
None, "--adapter", help="Override default adapter"
|
|
2209
|
-
),
|
|
2210
|
-
) -> None:
|
|
2211
|
-
"""Show detailed ticket information.
|
|
2212
|
-
|
|
2213
|
-
DEPRECATED: Use 'mcp-ticketer ticket show' instead.
|
|
2214
|
-
"""
|
|
2215
|
-
console.print(
|
|
2216
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
|
|
2217
|
-
)
|
|
2218
|
-
|
|
2219
|
-
async def _show() -> None:
|
|
2220
|
-
adapter_instance = get_adapter(
|
|
2221
|
-
override_adapter=adapter.value if adapter else None
|
|
2222
|
-
)
|
|
2223
|
-
ticket = await adapter_instance.read(ticket_id)
|
|
2224
|
-
ticket_comments = None
|
|
2225
|
-
if comments and ticket:
|
|
2226
|
-
ticket_comments = await adapter_instance.get_comments(ticket_id)
|
|
2227
|
-
return ticket, ticket_comments
|
|
2228
|
-
|
|
2229
|
-
ticket, ticket_comments = asyncio.run(_show())
|
|
2230
|
-
|
|
2231
|
-
if not ticket:
|
|
2232
|
-
console.print(f"[red]ā[/red] Ticket not found: {ticket_id}")
|
|
2233
|
-
raise typer.Exit(1) from None
|
|
2234
|
-
|
|
2235
|
-
# Display ticket details
|
|
2236
|
-
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
2237
|
-
console.print(f"Title: {ticket.title}")
|
|
2238
|
-
console.print(f"State: [green]{ticket.state}[/green]")
|
|
2239
|
-
console.print(f"Priority: [yellow]{ticket.priority}[/yellow]")
|
|
2240
|
-
|
|
2241
|
-
if ticket.description:
|
|
2242
|
-
console.print("\n[dim]Description:[/dim]")
|
|
2243
|
-
console.print(ticket.description)
|
|
2244
|
-
|
|
2245
|
-
if ticket.tags:
|
|
2246
|
-
console.print(f"\nTags: {', '.join(ticket.tags)}")
|
|
2247
|
-
|
|
2248
|
-
if ticket.assignee:
|
|
2249
|
-
console.print(f"Assignee: {ticket.assignee}")
|
|
2250
|
-
|
|
2251
|
-
# Display comments if requested
|
|
2252
|
-
if ticket_comments:
|
|
2253
|
-
console.print(f"\n[bold]Comments ({len(ticket_comments)}):[/bold]")
|
|
2254
|
-
for comment in ticket_comments:
|
|
2255
|
-
console.print(f"\n[dim]{comment.created_at} - {comment.author}:[/dim]")
|
|
2256
|
-
console.print(comment.content)
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
@app.command(deprecated=True, hidden=True)
|
|
2260
|
-
def comment(
|
|
2261
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2262
|
-
content: str = typer.Argument(..., help="Comment content"),
|
|
2263
|
-
adapter: AdapterType | None = typer.Option(
|
|
2264
|
-
None, "--adapter", help="Override default adapter"
|
|
2265
|
-
),
|
|
2266
|
-
) -> None:
|
|
2267
|
-
"""Add a comment to a ticket.
|
|
2268
|
-
|
|
2269
|
-
DEPRECATED: Use 'mcp-ticketer ticket comment' instead.
|
|
2270
|
-
"""
|
|
2271
|
-
console.print(
|
|
2272
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
|
|
2273
|
-
)
|
|
2274
|
-
|
|
2275
|
-
async def _comment() -> None:
|
|
2276
|
-
adapter_instance = get_adapter(
|
|
2277
|
-
override_adapter=adapter.value if adapter else None
|
|
2278
|
-
)
|
|
2279
|
-
|
|
2280
|
-
# Create comment
|
|
2281
|
-
comment = Comment(
|
|
2282
|
-
ticket_id=ticket_id,
|
|
2283
|
-
content=content,
|
|
2284
|
-
author="cli-user", # Could be made configurable
|
|
2285
|
-
)
|
|
2286
|
-
|
|
2287
|
-
result = await adapter_instance.add_comment(comment)
|
|
2288
|
-
return result
|
|
2289
|
-
|
|
2290
|
-
try:
|
|
2291
|
-
result = asyncio.run(_comment())
|
|
2292
|
-
console.print("[green]ā[/green] Comment added successfully")
|
|
2293
|
-
if result.id:
|
|
2294
|
-
console.print(f"Comment ID: {result.id}")
|
|
2295
|
-
console.print(f"Content: {content}")
|
|
2296
|
-
except Exception as e:
|
|
2297
|
-
console.print(f"[red]ā[/red] Failed to add comment: {e}")
|
|
2298
|
-
raise typer.Exit(1) from e
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
@app.command(deprecated=True, hidden=True)
|
|
2302
|
-
def update(
|
|
2303
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2304
|
-
title: str | None = typer.Option(None, "--title", help="New title"),
|
|
2305
|
-
description: str | None = typer.Option(
|
|
2306
|
-
None, "--description", "-d", help="New description"
|
|
2307
|
-
),
|
|
2308
|
-
priority: Priority | None = typer.Option(
|
|
2309
|
-
None, "--priority", "-p", help="New priority"
|
|
2310
|
-
),
|
|
2311
|
-
assignee: str | None = typer.Option(None, "--assignee", "-a", help="New assignee"),
|
|
2312
|
-
adapter: AdapterType | None = typer.Option(
|
|
2313
|
-
None, "--adapter", help="Override default adapter"
|
|
2314
|
-
),
|
|
2315
|
-
) -> None:
|
|
2316
|
-
"""Update ticket fields.
|
|
2317
|
-
|
|
2318
|
-
DEPRECATED: Use 'mcp-ticketer ticket update' instead.
|
|
2319
|
-
"""
|
|
2320
|
-
console.print(
|
|
2321
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket update' instead.[/yellow]\n"
|
|
2322
|
-
)
|
|
2323
|
-
updates = {}
|
|
2324
|
-
if title:
|
|
2325
|
-
updates["title"] = title
|
|
2326
|
-
if description:
|
|
2327
|
-
updates["description"] = description
|
|
2328
|
-
if priority:
|
|
2329
|
-
updates["priority"] = (
|
|
2330
|
-
priority.value if isinstance(priority, Priority) else priority
|
|
2331
|
-
)
|
|
2332
|
-
if assignee:
|
|
2333
|
-
updates["assignee"] = assignee
|
|
2334
|
-
|
|
2335
|
-
if not updates:
|
|
2336
|
-
console.print("[yellow]No updates specified[/yellow]")
|
|
2337
|
-
raise typer.Exit(1) from None
|
|
2338
|
-
|
|
2339
|
-
# Get the adapter name
|
|
2340
|
-
config = load_config()
|
|
2341
|
-
adapter_name = (
|
|
2342
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
2343
|
-
)
|
|
2344
|
-
|
|
2345
|
-
# Add ticket_id to updates
|
|
2346
|
-
updates["ticket_id"] = ticket_id
|
|
2347
|
-
|
|
2348
|
-
# Add to queue with explicit project directory
|
|
2349
|
-
queue = Queue()
|
|
2350
|
-
queue_id = queue.add(
|
|
2351
|
-
ticket_data=updates,
|
|
2352
|
-
adapter=adapter_name,
|
|
2353
|
-
operation="update",
|
|
2354
|
-
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
2355
|
-
)
|
|
2356
|
-
|
|
2357
|
-
console.print(f"[green]ā[/green] Queued ticket update: {queue_id}")
|
|
2358
|
-
for key, value in updates.items():
|
|
2359
|
-
if key != "ticket_id":
|
|
2360
|
-
console.print(f" {key}: {value}")
|
|
2361
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
2362
|
-
|
|
2363
|
-
# Start worker if needed
|
|
2364
|
-
manager = WorkerManager()
|
|
2365
|
-
if manager.start_if_needed():
|
|
2366
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
@app.command(deprecated=True, hidden=True)
|
|
2370
|
-
def transition(
|
|
2371
|
-
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
2372
|
-
state_positional: TicketState | None = typer.Argument(
|
|
2373
|
-
None, help="Target state (positional - deprecated, use --state instead)"
|
|
2374
|
-
),
|
|
2375
|
-
state: TicketState | None = typer.Option(
|
|
2376
|
-
None, "--state", "-s", help="Target state (recommended)"
|
|
2377
|
-
),
|
|
2378
|
-
adapter: AdapterType | None = typer.Option(
|
|
2379
|
-
None, "--adapter", help="Override default adapter"
|
|
2380
|
-
),
|
|
2381
|
-
) -> None:
|
|
2382
|
-
"""Change ticket state with validation.
|
|
2383
|
-
|
|
2384
|
-
DEPRECATED: Use 'mcp-ticketer ticket transition' instead.
|
|
2385
|
-
|
|
2386
|
-
Examples:
|
|
2387
|
-
# Recommended syntax with flag:
|
|
2388
|
-
mcp-ticketer ticket transition BTA-215 --state done
|
|
2389
|
-
mcp-ticketer ticket transition BTA-215 -s in_progress
|
|
2390
|
-
|
|
2391
|
-
# Legacy positional syntax (still supported):
|
|
2392
|
-
mcp-ticketer ticket transition BTA-215 done
|
|
2393
|
-
|
|
2394
|
-
"""
|
|
2395
|
-
console.print(
|
|
2396
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket transition' instead.[/yellow]\n"
|
|
2397
|
-
)
|
|
2398
|
-
|
|
2399
|
-
# Determine which state to use (prefer flag over positional)
|
|
2400
|
-
target_state = state if state is not None else state_positional
|
|
2401
|
-
|
|
2402
|
-
if target_state is None:
|
|
2403
|
-
console.print("[red]Error: State is required[/red]")
|
|
2404
|
-
console.print(
|
|
2405
|
-
"Use either:\n"
|
|
2406
|
-
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
2407
|
-
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
2408
|
-
)
|
|
2409
|
-
raise typer.Exit(1) from None
|
|
2410
|
-
|
|
2411
|
-
# Get the adapter name
|
|
2412
|
-
config = load_config()
|
|
2413
|
-
adapter_name = (
|
|
2414
|
-
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
2415
|
-
)
|
|
2416
|
-
|
|
2417
|
-
# Add to queue with explicit project directory
|
|
2418
|
-
queue = Queue()
|
|
2419
|
-
queue_id = queue.add(
|
|
2420
|
-
ticket_data={
|
|
2421
|
-
"ticket_id": ticket_id,
|
|
2422
|
-
"state": (
|
|
2423
|
-
target_state.value if hasattr(target_state, "value") else target_state
|
|
2424
|
-
),
|
|
2425
|
-
},
|
|
2426
|
-
adapter=adapter_name,
|
|
2427
|
-
operation="transition",
|
|
2428
|
-
project_dir=str(Path.cwd()), # Explicitly pass current project directory
|
|
2429
|
-
)
|
|
2430
|
-
|
|
2431
|
-
console.print(f"[green]ā[/green] Queued state transition: {queue_id}")
|
|
2432
|
-
console.print(f" Ticket: {ticket_id} ā {target_state}")
|
|
2433
|
-
console.print("[dim]Use 'mcp-ticketer status {queue_id}' to check progress[/dim]")
|
|
2434
|
-
|
|
2435
|
-
# Start worker if needed
|
|
2436
|
-
manager = WorkerManager()
|
|
2437
|
-
if manager.start_if_needed():
|
|
2438
|
-
console.print("[dim]Worker started to process request[/dim]")
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
@app.command(deprecated=True, hidden=True)
|
|
2442
|
-
def search(
|
|
2443
|
-
query: str | None = typer.Argument(None, help="Search query"),
|
|
2444
|
-
state: TicketState | None = typer.Option(None, "--state", "-s"),
|
|
2445
|
-
priority: Priority | None = typer.Option(None, "--priority", "-p"),
|
|
2446
|
-
assignee: str | None = typer.Option(None, "--assignee", "-a"),
|
|
2447
|
-
limit: int = typer.Option(10, "--limit", "-l"),
|
|
2448
|
-
adapter: AdapterType | None = typer.Option(
|
|
2449
|
-
None, "--adapter", help="Override default adapter"
|
|
2450
|
-
),
|
|
2451
|
-
) -> None:
|
|
2452
|
-
"""Search tickets with advanced query.
|
|
2453
|
-
|
|
2454
|
-
DEPRECATED: Use 'mcp-ticketer ticket search' instead.
|
|
2455
|
-
"""
|
|
2456
|
-
console.print(
|
|
2457
|
-
"[yellow]ā ļø This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
|
|
2458
|
-
)
|
|
2459
|
-
|
|
2460
|
-
async def _search() -> None:
|
|
2461
|
-
adapter_instance = get_adapter(
|
|
2462
|
-
override_adapter=adapter.value if adapter else None
|
|
2463
|
-
)
|
|
2464
|
-
search_query = SearchQuery(
|
|
2465
|
-
query=query,
|
|
2466
|
-
state=state,
|
|
2467
|
-
priority=priority,
|
|
2468
|
-
assignee=assignee,
|
|
2469
|
-
limit=limit,
|
|
2470
|
-
)
|
|
2471
|
-
return await adapter_instance.search(search_query)
|
|
2472
|
-
|
|
2473
|
-
tickets = asyncio.run(_search())
|
|
2474
|
-
|
|
2475
|
-
if not tickets:
|
|
2476
|
-
console.print("[yellow]No tickets found matching query[/yellow]")
|
|
2477
|
-
return
|
|
2478
|
-
|
|
2479
|
-
# Display results
|
|
2480
|
-
console.print(f"\n[bold]Found {len(tickets)} ticket(s)[/bold]\n")
|
|
2481
|
-
|
|
2482
|
-
for ticket in tickets:
|
|
2483
|
-
console.print(f"[cyan]{ticket.id}[/cyan]: {ticket.title}")
|
|
2484
|
-
console.print(f" State: {ticket.state} | Priority: {ticket.priority}")
|
|
2485
|
-
if ticket.assignee:
|
|
2486
|
-
console.print(f" Assignee: {ticket.assignee}")
|
|
2487
|
-
console.print()
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
# Add ticket command group to main app
|
|
2491
|
-
app.add_typer(ticket_app, name="ticket")
|
|
2492
|
-
|
|
2493
|
-
# Add platform command group to main app
|
|
2494
|
-
app.add_typer(platform_app, name="platform")
|
|
2495
|
-
|
|
2496
|
-
# Add queue command to main app
|
|
2497
|
-
app.add_typer(queue_app, name="queue")
|
|
2498
|
-
|
|
2499
|
-
# Add discover command to main app
|
|
2500
|
-
app.add_typer(discover_app, name="discover")
|
|
2501
|
-
|
|
2502
|
-
# Add instructions command to main app
|
|
2503
|
-
app.add_typer(instruction_app, name="instructions")
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
# Add diagnostics command
|
|
2507
|
-
@app.command("doctor")
|
|
2508
|
-
def doctor_command(
|
|
2509
|
-
output_file: str | None = typer.Option(
|
|
2510
|
-
None, "--output", "-o", help="Save full report to file"
|
|
2511
|
-
),
|
|
2512
|
-
json_output: bool = typer.Option(
|
|
2513
|
-
False, "--json", help="Output report in JSON format"
|
|
2514
|
-
),
|
|
2515
|
-
simple: bool = typer.Option(
|
|
2516
|
-
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
2517
|
-
),
|
|
2518
|
-
) -> None:
|
|
2519
|
-
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
2520
|
-
if simple:
|
|
2521
|
-
from .simple_health import simple_diagnose
|
|
2522
|
-
|
|
2523
|
-
report = simple_diagnose()
|
|
2524
|
-
if output_file:
|
|
2525
|
-
import json
|
|
2526
|
-
|
|
2527
|
-
with open(output_file, "w") as f:
|
|
2528
|
-
json.dump(report, f, indent=2)
|
|
2529
|
-
console.print(f"\nš Report saved to: {output_file}")
|
|
2530
|
-
if json_output:
|
|
2531
|
-
import json
|
|
2532
|
-
|
|
2533
|
-
console.print("\n" + json.dumps(report, indent=2))
|
|
2534
|
-
if report["issues"]:
|
|
2535
|
-
raise typer.Exit(1) from None
|
|
2536
|
-
else:
|
|
2537
|
-
try:
|
|
2538
|
-
asyncio.run(
|
|
2539
|
-
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
2540
|
-
)
|
|
2541
|
-
except typer.Exit:
|
|
2542
|
-
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
2543
|
-
raise
|
|
2544
|
-
except Exception as e:
|
|
2545
|
-
console.print(f"ā ļø Full diagnostics failed: {e}")
|
|
2546
|
-
console.print("š Falling back to simple diagnostics...")
|
|
2547
|
-
from .simple_health import simple_diagnose
|
|
2548
|
-
|
|
2549
|
-
report = simple_diagnose()
|
|
2550
|
-
if report["issues"]:
|
|
2551
|
-
raise typer.Exit(1) from None
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
@app.command("diagnose", hidden=True)
|
|
2555
|
-
def diagnose_alias(
|
|
2556
|
-
output_file: str | None = typer.Option(
|
|
2557
|
-
None, "--output", "-o", help="Save full report to file"
|
|
2558
|
-
),
|
|
2559
|
-
json_output: bool = typer.Option(
|
|
2560
|
-
False, "--json", help="Output report in JSON format"
|
|
2561
|
-
),
|
|
2562
|
-
simple: bool = typer.Option(
|
|
2563
|
-
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
2564
|
-
),
|
|
2565
|
-
) -> None:
|
|
2566
|
-
"""Run comprehensive system diagnostics and health check (alias for doctor)."""
|
|
2567
|
-
# Call the doctor_command function with the same parameters
|
|
2568
|
-
doctor_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
@app.command("status")
|
|
2572
|
-
def status_command() -> None:
|
|
2573
|
-
"""Quick health check - shows system status summary (alias: health)."""
|
|
2574
|
-
from .simple_health import simple_health_check
|
|
2575
|
-
|
|
2576
|
-
result = simple_health_check()
|
|
2577
|
-
if result != 0:
|
|
2578
|
-
raise typer.Exit(result) from None
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
@app.command("health")
|
|
2582
|
-
def health_alias() -> None:
|
|
2583
|
-
"""Quick health check - shows system status summary (alias for status)."""
|
|
2584
|
-
from .simple_health import simple_health_check
|
|
2585
|
-
|
|
2586
|
-
result = simple_health_check()
|
|
2587
|
-
if result != 0:
|
|
2588
|
-
raise typer.Exit(result) from None
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
# Create MCP configuration command group
|
|
2592
|
-
mcp_app = typer.Typer(
|
|
2593
|
-
name="mcp",
|
|
2594
|
-
help="Configure MCP integration for AI clients (Claude, Gemini, Codex, Auggie)",
|
|
2595
|
-
add_completion=False,
|
|
2596
|
-
invoke_without_command=True,
|
|
2597
|
-
)
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
@mcp_app.callback()
|
|
2601
|
-
def mcp_callback(
|
|
2602
|
-
ctx: typer.Context,
|
|
2603
|
-
project_path: str | None = typer.Option(
|
|
2604
|
-
None, "--path", "-p", help="Project directory path (default: current directory)"
|
|
2605
|
-
),
|
|
2606
|
-
) -> None:
|
|
2607
|
-
"""MCP command group - runs MCP server if no subcommand provided.
|
|
2608
|
-
|
|
2609
|
-
Examples:
|
|
2610
|
-
mcp-ticketer mcp # Start server in current directory
|
|
2611
|
-
mcp-ticketer mcp --path /dir # Start server in specific directory
|
|
2612
|
-
mcp-ticketer mcp -p /dir # Start server (short form)
|
|
2613
|
-
mcp-ticketer mcp status # Check MCP status
|
|
2614
|
-
mcp-ticketer mcp serve # Explicitly start server
|
|
2615
|
-
|
|
2616
|
-
"""
|
|
2617
|
-
if ctx.invoked_subcommand is None:
|
|
2618
|
-
# No subcommand provided, run the serve command
|
|
2619
|
-
# Change to project directory if provided
|
|
2620
|
-
if project_path:
|
|
2621
|
-
import os
|
|
2622
|
-
|
|
2623
|
-
os.chdir(project_path)
|
|
2624
|
-
# Invoke the serve command through context
|
|
2625
|
-
ctx.invoke(mcp_serve, adapter=None, base_path=None)
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
@app.command()
|
|
2629
|
-
def install(
|
|
2630
|
-
platform: str | None = typer.Argument(
|
|
2631
|
-
None,
|
|
2632
|
-
help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
|
|
2633
|
-
),
|
|
2634
|
-
auto_detect: bool = typer.Option(
|
|
2635
|
-
False,
|
|
2636
|
-
"--auto-detect",
|
|
2637
|
-
"-d",
|
|
2638
|
-
help="Auto-detect and show all installed AI platforms",
|
|
2639
|
-
),
|
|
2640
|
-
install_all: bool = typer.Option(
|
|
2641
|
-
False,
|
|
2642
|
-
"--all",
|
|
2643
|
-
help="Install for all detected platforms",
|
|
2644
|
-
),
|
|
2645
|
-
adapter: str | None = typer.Option(
|
|
2646
|
-
None,
|
|
2647
|
-
"--adapter",
|
|
2648
|
-
"-a",
|
|
2649
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
2650
|
-
),
|
|
2651
|
-
project_path: str | None = typer.Option(
|
|
2652
|
-
None, "--path", help="Project path (default: current directory)"
|
|
329
|
+
@app.command("set")
|
|
330
|
+
def set_config(
|
|
331
|
+
adapter: AdapterType | None = typer.Option(
|
|
332
|
+
None, "--adapter", "-a", help="Set default adapter"
|
|
2653
333
|
),
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
"--global",
|
|
2657
|
-
"-g",
|
|
2658
|
-
help="Save to global config instead of project-specific",
|
|
334
|
+
team_key: str | None = typer.Option(
|
|
335
|
+
None, "--team-key", help="Linear team key (e.g., BTA)"
|
|
2659
336
|
),
|
|
337
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Linear team ID"),
|
|
338
|
+
owner: str | None = typer.Option(None, "--owner", help="GitHub repository owner"),
|
|
339
|
+
repo: str | None = typer.Option(None, "--repo", help="GitHub repository name"),
|
|
340
|
+
server: str | None = typer.Option(None, "--server", help="JIRA server URL"),
|
|
341
|
+
project: str | None = typer.Option(None, "--project", help="JIRA project key"),
|
|
2660
342
|
base_path: str | None = typer.Option(
|
|
2661
|
-
None,
|
|
2662
|
-
"--base-path",
|
|
2663
|
-
"-p",
|
|
2664
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
2665
|
-
),
|
|
2666
|
-
api_key: str | None = typer.Option(
|
|
2667
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
2668
|
-
),
|
|
2669
|
-
team_id: str | None = typer.Option(
|
|
2670
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
2671
|
-
),
|
|
2672
|
-
jira_server: str | None = typer.Option(
|
|
2673
|
-
None,
|
|
2674
|
-
"--jira-server",
|
|
2675
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
2676
|
-
),
|
|
2677
|
-
jira_email: str | None = typer.Option(
|
|
2678
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
2679
|
-
),
|
|
2680
|
-
jira_project: str | None = typer.Option(
|
|
2681
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
2682
|
-
),
|
|
2683
|
-
github_owner: str | None = typer.Option(
|
|
2684
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
2685
|
-
),
|
|
2686
|
-
github_repo: str | None = typer.Option(
|
|
2687
|
-
None, "--github-repo", help="GitHub repository name"
|
|
2688
|
-
),
|
|
2689
|
-
github_token: str | None = typer.Option(
|
|
2690
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
2691
|
-
),
|
|
2692
|
-
dry_run: bool = typer.Option(
|
|
2693
|
-
False,
|
|
2694
|
-
"--dry-run",
|
|
2695
|
-
help="Show what would be done without making changes (for platform installation)",
|
|
343
|
+
None, "--base-path", help="AITrackdown base path"
|
|
2696
344
|
),
|
|
2697
345
|
) -> None:
|
|
2698
|
-
"""
|
|
2699
|
-
|
|
2700
|
-
This command configures mcp-ticketer as an MCP server for various AI
|
|
2701
|
-
platforms. It updates platform-specific configuration files to enable
|
|
2702
|
-
mcp-ticketer integration.
|
|
2703
|
-
|
|
2704
|
-
RECOMMENDED: Use 'mcp-ticketer setup' for first-time setup, which
|
|
2705
|
-
handles both adapter configuration and platform installation together.
|
|
2706
|
-
|
|
2707
|
-
Platform Installation:
|
|
2708
|
-
# Auto-detect and prompt for platform selection
|
|
2709
|
-
mcp-ticketer install
|
|
2710
|
-
|
|
2711
|
-
# Show all detected platforms
|
|
2712
|
-
mcp-ticketer install --auto-detect
|
|
2713
|
-
|
|
2714
|
-
# Install for all detected platforms
|
|
2715
|
-
mcp-ticketer install --all
|
|
2716
|
-
|
|
2717
|
-
# Install for specific platform
|
|
2718
|
-
mcp-ticketer install claude-code # Claude Code (project-level)
|
|
2719
|
-
mcp-ticketer install claude-desktop # Claude Desktop (global)
|
|
2720
|
-
mcp-ticketer install gemini # Gemini CLI
|
|
2721
|
-
mcp-ticketer install codex # Codex
|
|
2722
|
-
mcp-ticketer install auggie # Auggie
|
|
2723
|
-
|
|
2724
|
-
Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
|
|
2725
|
-
mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
|
|
346
|
+
"""Set default adapter and adapter-specific configuration.
|
|
2726
347
|
|
|
348
|
+
When called without arguments, shows current configuration.
|
|
2727
349
|
"""
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
# Handle auto-detect flag (just show detected platforms and exit)
|
|
2733
|
-
if auto_detect:
|
|
2734
|
-
detected = detector.detect_all(
|
|
2735
|
-
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2736
|
-
)
|
|
2737
|
-
|
|
2738
|
-
if not detected:
|
|
2739
|
-
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2740
|
-
console.print("\n[bold]Supported platforms:[/bold]")
|
|
2741
|
-
console.print(" ⢠Claude Code - Project-level configuration")
|
|
2742
|
-
console.print(" ⢠Claude Desktop - Global GUI application")
|
|
2743
|
-
console.print(" ⢠Auggie - CLI tool with global config")
|
|
2744
|
-
console.print(" ⢠Codex - CLI tool with global config")
|
|
2745
|
-
console.print(" ⢠Gemini - CLI tool with project/global config")
|
|
2746
|
-
console.print(
|
|
2747
|
-
"\n[dim]Install these platforms to use them with mcp-ticketer.[/dim]"
|
|
2748
|
-
)
|
|
2749
|
-
return
|
|
2750
|
-
|
|
2751
|
-
console.print("[bold]Detected AI platforms:[/bold]\n")
|
|
2752
|
-
table = Table(show_header=True, header_style="bold cyan")
|
|
2753
|
-
table.add_column("Platform", style="green")
|
|
2754
|
-
table.add_column("Status", style="yellow")
|
|
2755
|
-
table.add_column("Scope", style="blue")
|
|
2756
|
-
table.add_column("Config Path", style="dim")
|
|
2757
|
-
|
|
2758
|
-
for plat in detected:
|
|
2759
|
-
status = "ā Installed" if plat.is_installed else "ā Config Issue"
|
|
2760
|
-
table.add_row(plat.display_name, status, plat.scope, str(plat.config_path))
|
|
2761
|
-
|
|
2762
|
-
console.print(table)
|
|
2763
|
-
console.print(
|
|
2764
|
-
"\n[dim]Run 'mcp-ticketer install <platform>' to configure a specific platform[/dim]"
|
|
2765
|
-
)
|
|
2766
|
-
console.print(
|
|
2767
|
-
"[dim]Run 'mcp-ticketer install --all' to configure all detected platforms[/dim]"
|
|
2768
|
-
)
|
|
2769
|
-
return
|
|
2770
|
-
|
|
2771
|
-
# Handle --all flag (install for all detected platforms)
|
|
2772
|
-
if install_all:
|
|
2773
|
-
detected = detector.detect_all(
|
|
2774
|
-
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2775
|
-
)
|
|
2776
|
-
|
|
2777
|
-
if not detected:
|
|
2778
|
-
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2779
|
-
console.print(
|
|
2780
|
-
"Run 'mcp-ticketer install --auto-detect' to see supported platforms."
|
|
2781
|
-
)
|
|
2782
|
-
return
|
|
2783
|
-
|
|
2784
|
-
# Handle dry-run mode - show what would be installed without actually installing
|
|
2785
|
-
if dry_run:
|
|
2786
|
-
console.print(
|
|
2787
|
-
"\n[yellow]DRY RUN - The following platforms would be configured:[/yellow]\n"
|
|
2788
|
-
)
|
|
2789
|
-
|
|
2790
|
-
installable_count = 0
|
|
2791
|
-
for plat in detected:
|
|
2792
|
-
if plat.is_installed:
|
|
2793
|
-
console.print(f" ā {plat.display_name} ({plat.scope})")
|
|
2794
|
-
installable_count += 1
|
|
2795
|
-
else:
|
|
2796
|
-
console.print(
|
|
2797
|
-
f" ā {plat.display_name} ({plat.scope}) - would be skipped (configuration issue)"
|
|
2798
|
-
)
|
|
2799
|
-
|
|
2800
|
-
console.print(
|
|
2801
|
-
f"\n[dim]Would configure {installable_count} platform(s)[/dim]"
|
|
2802
|
-
)
|
|
2803
|
-
return
|
|
2804
|
-
|
|
2805
|
-
console.print(
|
|
2806
|
-
f"[bold]Installing for {len(detected)} detected platform(s)...[/bold]\n"
|
|
2807
|
-
)
|
|
2808
|
-
|
|
2809
|
-
# Import configuration functions
|
|
2810
|
-
from .auggie_configure import configure_auggie_mcp
|
|
2811
|
-
from .codex_configure import configure_codex_mcp
|
|
2812
|
-
from .gemini_configure import configure_gemini_mcp
|
|
2813
|
-
from .mcp_configure import configure_claude_mcp
|
|
2814
|
-
|
|
2815
|
-
# Map platform names to configuration functions
|
|
2816
|
-
platform_mapping = {
|
|
2817
|
-
"claude-code": lambda: configure_claude_mcp(
|
|
2818
|
-
global_config=False, force=True
|
|
2819
|
-
),
|
|
2820
|
-
"claude-desktop": lambda: configure_claude_mcp(
|
|
2821
|
-
global_config=True, force=True
|
|
2822
|
-
),
|
|
2823
|
-
"auggie": lambda: configure_auggie_mcp(force=True),
|
|
2824
|
-
"gemini": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
2825
|
-
"codex": lambda: configure_codex_mcp(force=True),
|
|
2826
|
-
}
|
|
2827
|
-
|
|
2828
|
-
success_count = 0
|
|
2829
|
-
failed = []
|
|
2830
|
-
|
|
2831
|
-
for plat in detected:
|
|
2832
|
-
if not plat.is_installed:
|
|
2833
|
-
console.print(
|
|
2834
|
-
f"[yellow]ā [/yellow] Skipping {plat.display_name} (configuration issue)"
|
|
2835
|
-
)
|
|
2836
|
-
continue
|
|
2837
|
-
|
|
2838
|
-
config_func = platform_mapping.get(plat.name)
|
|
2839
|
-
if not config_func:
|
|
2840
|
-
console.print(
|
|
2841
|
-
f"[yellow]ā [/yellow] No installer for {plat.display_name}"
|
|
2842
|
-
)
|
|
2843
|
-
continue
|
|
2844
|
-
|
|
2845
|
-
try:
|
|
2846
|
-
console.print(f"[cyan]Installing for {plat.display_name}...[/cyan]")
|
|
2847
|
-
config_func()
|
|
2848
|
-
success_count += 1
|
|
2849
|
-
except Exception as e:
|
|
2850
|
-
console.print(
|
|
2851
|
-
f"[red]ā[/red] Failed to install for {plat.display_name}: {e}"
|
|
2852
|
-
)
|
|
2853
|
-
failed.append(plat.display_name)
|
|
2854
|
-
|
|
2855
|
-
console.print(
|
|
2856
|
-
f"\n[bold]Installation complete:[/bold] {success_count} succeeded"
|
|
2857
|
-
)
|
|
2858
|
-
if failed:
|
|
2859
|
-
console.print(f"[red]Failed:[/red] {', '.join(failed)}")
|
|
2860
|
-
return
|
|
2861
|
-
|
|
2862
|
-
# If no platform argument and no adapter flag, auto-detect and prompt
|
|
2863
|
-
if platform is None and adapter is None:
|
|
2864
|
-
detected = detector.detect_all(
|
|
2865
|
-
project_path=Path(project_path) if project_path else Path.cwd()
|
|
2866
|
-
)
|
|
2867
|
-
|
|
2868
|
-
# Filter to only installed platforms
|
|
2869
|
-
installed = [p for p in detected if p.is_installed]
|
|
2870
|
-
|
|
2871
|
-
if not installed:
|
|
2872
|
-
console.print("[yellow]No AI platforms detected.[/yellow]")
|
|
2873
|
-
console.print("\n[bold]To see supported platforms:[/bold]")
|
|
2874
|
-
console.print(" mcp-ticketer install --auto-detect")
|
|
2875
|
-
console.print("\n[bold]Or run legacy adapter setup:[/bold]")
|
|
2876
|
-
console.print(" mcp-ticketer install --adapter <adapter-type>")
|
|
2877
|
-
return
|
|
2878
|
-
|
|
2879
|
-
# Show detected platforms and prompt for selection
|
|
2880
|
-
console.print("[bold]Detected AI platforms:[/bold]\n")
|
|
2881
|
-
for idx, plat in enumerate(installed, 1):
|
|
2882
|
-
console.print(f" {idx}. {plat.display_name} ({plat.scope})")
|
|
2883
|
-
|
|
350
|
+
if not any([adapter, team_key, team_id, owner, repo, server, project, base_path]):
|
|
351
|
+
# Show current configuration
|
|
352
|
+
config = load_config()
|
|
353
|
+
console.print("[bold]Current Configuration:[/bold]")
|
|
2884
354
|
console.print(
|
|
2885
|
-
"
|
|
2886
|
-
)
|
|
2887
|
-
choice = typer.prompt("Select platform")
|
|
2888
|
-
|
|
2889
|
-
if choice.lower() == "q":
|
|
2890
|
-
console.print("Installation cancelled.")
|
|
2891
|
-
return
|
|
2892
|
-
|
|
2893
|
-
try:
|
|
2894
|
-
idx = int(choice) - 1
|
|
2895
|
-
if idx < 0 or idx >= len(installed):
|
|
2896
|
-
console.print("[red]Invalid selection.[/red]")
|
|
2897
|
-
raise typer.Exit(1) from None
|
|
2898
|
-
platform = installed[idx].name
|
|
2899
|
-
except ValueError as e:
|
|
2900
|
-
console.print("[red]Invalid input. Please enter a number.[/red]")
|
|
2901
|
-
raise typer.Exit(1) from e
|
|
2902
|
-
|
|
2903
|
-
# If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
|
|
2904
|
-
if platform is not None:
|
|
2905
|
-
# Validate that the platform is actually installed
|
|
2906
|
-
platform_info = get_platform_by_name(
|
|
2907
|
-
platform, project_path=Path(project_path) if project_path else Path.cwd()
|
|
355
|
+
f"Default adapter: [cyan]{config.get('default_adapter', 'aitrackdown')}[/cyan]"
|
|
2908
356
|
)
|
|
2909
357
|
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
)
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
# Platform not detected at all - warn but allow proceeding
|
|
2925
|
-
console.print(
|
|
2926
|
-
f"[yellow]ā [/yellow] Platform '{platform}' not detected on this system."
|
|
2927
|
-
)
|
|
2928
|
-
console.print(
|
|
2929
|
-
"[dim]Run 'mcp-ticketer install --auto-detect' to see detected platforms.[/dim]\n"
|
|
2930
|
-
)
|
|
2931
|
-
|
|
2932
|
-
proceed = typer.confirm(
|
|
2933
|
-
"Do you want to proceed with installation anyway?", default=False
|
|
2934
|
-
)
|
|
2935
|
-
if not proceed:
|
|
2936
|
-
console.print("Installation cancelled.")
|
|
2937
|
-
return
|
|
2938
|
-
|
|
2939
|
-
# Import configuration functions
|
|
2940
|
-
from .auggie_configure import configure_auggie_mcp
|
|
2941
|
-
from .codex_configure import configure_codex_mcp
|
|
2942
|
-
from .gemini_configure import configure_gemini_mcp
|
|
2943
|
-
from .mcp_configure import configure_claude_mcp
|
|
2944
|
-
|
|
2945
|
-
# Map platform names to configuration functions
|
|
2946
|
-
platform_mapping = {
|
|
2947
|
-
"claude-code": {
|
|
2948
|
-
"func": lambda: configure_claude_mcp(global_config=False, force=True),
|
|
2949
|
-
"name": "Claude Code",
|
|
2950
|
-
},
|
|
2951
|
-
"claude-desktop": {
|
|
2952
|
-
"func": lambda: configure_claude_mcp(global_config=True, force=True),
|
|
2953
|
-
"name": "Claude Desktop",
|
|
2954
|
-
},
|
|
2955
|
-
"auggie": {
|
|
2956
|
-
"func": lambda: configure_auggie_mcp(force=True),
|
|
2957
|
-
"name": "Auggie",
|
|
2958
|
-
},
|
|
2959
|
-
"gemini": {
|
|
2960
|
-
"func": lambda: configure_gemini_mcp(scope="project", force=True),
|
|
2961
|
-
"name": "Gemini CLI",
|
|
2962
|
-
},
|
|
2963
|
-
"codex": {
|
|
2964
|
-
"func": lambda: configure_codex_mcp(force=True),
|
|
2965
|
-
"name": "Codex",
|
|
2966
|
-
},
|
|
2967
|
-
}
|
|
2968
|
-
|
|
2969
|
-
if platform not in platform_mapping:
|
|
2970
|
-
console.print(f"[red]Unknown platform: {platform}[/red]")
|
|
2971
|
-
console.print("\n[bold]Available platforms:[/bold]")
|
|
2972
|
-
for p in platform_mapping.keys():
|
|
2973
|
-
console.print(f" ⢠{p}")
|
|
2974
|
-
raise typer.Exit(1) from None
|
|
2975
|
-
|
|
2976
|
-
config = platform_mapping[platform]
|
|
2977
|
-
|
|
2978
|
-
if dry_run:
|
|
2979
|
-
console.print(f"[cyan]DRY RUN - Would install for {config['name']}[/cyan]")
|
|
2980
|
-
return
|
|
2981
|
-
|
|
2982
|
-
try:
|
|
2983
|
-
config["func"]()
|
|
2984
|
-
except Exception as e:
|
|
2985
|
-
console.print(f"[red]Installation failed: {e}[/red]")
|
|
2986
|
-
raise typer.Exit(1) from e
|
|
2987
|
-
return
|
|
2988
|
-
|
|
2989
|
-
# Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
|
|
2990
|
-
# This makes 'install' and 'init' synonymous when called without platform argument
|
|
2991
|
-
init(
|
|
2992
|
-
adapter=adapter,
|
|
2993
|
-
project_path=project_path,
|
|
2994
|
-
global_config=global_config,
|
|
2995
|
-
base_path=base_path,
|
|
2996
|
-
api_key=api_key,
|
|
2997
|
-
team_id=team_id,
|
|
2998
|
-
jira_server=jira_server,
|
|
2999
|
-
jira_email=jira_email,
|
|
3000
|
-
jira_project=jira_project,
|
|
3001
|
-
github_owner=github_owner,
|
|
3002
|
-
github_repo=github_repo,
|
|
3003
|
-
github_token=github_token,
|
|
3004
|
-
)
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
@app.command()
|
|
3008
|
-
def remove(
|
|
3009
|
-
platform: str | None = typer.Argument(
|
|
3010
|
-
None,
|
|
3011
|
-
help="Platform to remove (claude-code, claude-desktop, auggie, gemini, codex)",
|
|
3012
|
-
),
|
|
3013
|
-
dry_run: bool = typer.Option(
|
|
3014
|
-
False, "--dry-run", help="Show what would be done without making changes"
|
|
3015
|
-
),
|
|
3016
|
-
) -> None:
|
|
3017
|
-
"""Remove mcp-ticketer from AI platforms.
|
|
3018
|
-
|
|
3019
|
-
Without arguments, shows help and available platforms.
|
|
3020
|
-
With a platform argument, removes MCP configuration for that platform.
|
|
3021
|
-
|
|
3022
|
-
Examples:
|
|
3023
|
-
# Remove from Claude Code (project-level)
|
|
3024
|
-
mcp-ticketer remove claude-code
|
|
3025
|
-
|
|
3026
|
-
# Remove from Claude Desktop (global)
|
|
3027
|
-
mcp-ticketer remove claude-desktop
|
|
3028
|
-
|
|
3029
|
-
# Remove from Auggie
|
|
3030
|
-
mcp-ticketer remove auggie
|
|
3031
|
-
|
|
3032
|
-
# Dry run to preview changes
|
|
3033
|
-
mcp-ticketer remove claude-code --dry-run
|
|
3034
|
-
|
|
3035
|
-
"""
|
|
3036
|
-
# If no platform specified, show help message
|
|
3037
|
-
if platform is None:
|
|
3038
|
-
console.print("[bold]Remove mcp-ticketer from AI platforms[/bold]\n")
|
|
3039
|
-
console.print("Usage: mcp-ticketer remove <platform>\n")
|
|
3040
|
-
console.print("[bold]Available platforms:[/bold]")
|
|
3041
|
-
console.print(" ⢠claude-code - Claude Code (project-level)")
|
|
3042
|
-
console.print(" ⢠claude-desktop - Claude Desktop (global)")
|
|
3043
|
-
console.print(" ⢠auggie - Auggie (global)")
|
|
3044
|
-
console.print(" ⢠gemini - Gemini CLI (project-level by default)")
|
|
3045
|
-
console.print(" ⢠codex - Codex (global)")
|
|
358
|
+
adapters_config = config.get("adapters", {})
|
|
359
|
+
if adapters_config:
|
|
360
|
+
console.print("\n[bold]Adapter Settings:[/bold]")
|
|
361
|
+
for adapter_name, adapter_config in adapters_config.items():
|
|
362
|
+
console.print(f"\n[cyan]{adapter_name}:[/cyan]")
|
|
363
|
+
for key, value in adapter_config.items():
|
|
364
|
+
# Don't display sensitive values like tokens
|
|
365
|
+
if (
|
|
366
|
+
"token" in key.lower()
|
|
367
|
+
or "key" in key.lower()
|
|
368
|
+
and "team" not in key.lower()
|
|
369
|
+
):
|
|
370
|
+
value = "***" if value else "not set"
|
|
371
|
+
console.print(f" {key}: {value}")
|
|
3046
372
|
return
|
|
3047
373
|
|
|
3048
|
-
|
|
3049
|
-
from .auggie_configure import remove_auggie_mcp
|
|
3050
|
-
from .codex_configure import remove_codex_mcp
|
|
3051
|
-
from .gemini_configure import remove_gemini_mcp
|
|
3052
|
-
from .mcp_configure import remove_claude_mcp
|
|
3053
|
-
|
|
3054
|
-
# Map platform names to removal functions
|
|
3055
|
-
platform_mapping = {
|
|
3056
|
-
"claude-code": {
|
|
3057
|
-
"func": lambda: remove_claude_mcp(global_config=False, dry_run=dry_run),
|
|
3058
|
-
"name": "Claude Code",
|
|
3059
|
-
},
|
|
3060
|
-
"claude-desktop": {
|
|
3061
|
-
"func": lambda: remove_claude_mcp(global_config=True, dry_run=dry_run),
|
|
3062
|
-
"name": "Claude Desktop",
|
|
3063
|
-
},
|
|
3064
|
-
"auggie": {
|
|
3065
|
-
"func": lambda: remove_auggie_mcp(dry_run=dry_run),
|
|
3066
|
-
"name": "Auggie",
|
|
3067
|
-
},
|
|
3068
|
-
"gemini": {
|
|
3069
|
-
"func": lambda: remove_gemini_mcp(scope="project", dry_run=dry_run),
|
|
3070
|
-
"name": "Gemini CLI",
|
|
3071
|
-
},
|
|
3072
|
-
"codex": {
|
|
3073
|
-
"func": lambda: remove_codex_mcp(dry_run=dry_run),
|
|
3074
|
-
"name": "Codex",
|
|
3075
|
-
},
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
if platform not in platform_mapping:
|
|
3079
|
-
console.print(f"[red]Unknown platform: {platform}[/red]")
|
|
3080
|
-
console.print("\n[bold]Available platforms:[/bold]")
|
|
3081
|
-
for p in platform_mapping.keys():
|
|
3082
|
-
console.print(f" ⢠{p}")
|
|
3083
|
-
raise typer.Exit(1) from None
|
|
3084
|
-
|
|
3085
|
-
config = platform_mapping[platform]
|
|
3086
|
-
|
|
3087
|
-
try:
|
|
3088
|
-
config["func"]()
|
|
3089
|
-
except Exception as e:
|
|
3090
|
-
console.print(f"[red]Removal failed: {e}[/red]")
|
|
3091
|
-
raise typer.Exit(1) from e
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
@app.command()
|
|
3095
|
-
def uninstall(
|
|
3096
|
-
platform: str | None = typer.Argument(
|
|
3097
|
-
None,
|
|
3098
|
-
help="Platform to uninstall (claude-code, claude-desktop, auggie, gemini, codex)",
|
|
3099
|
-
),
|
|
3100
|
-
dry_run: bool = typer.Option(
|
|
3101
|
-
False, "--dry-run", help="Show what would be done without making changes"
|
|
3102
|
-
),
|
|
3103
|
-
) -> None:
|
|
3104
|
-
"""Uninstall mcp-ticketer from AI platforms (alias for remove).
|
|
3105
|
-
|
|
3106
|
-
This is an alias for the 'remove' command.
|
|
3107
|
-
|
|
3108
|
-
Without arguments, shows help and available platforms.
|
|
3109
|
-
With a platform argument, removes MCP configuration for that platform.
|
|
3110
|
-
|
|
3111
|
-
Examples:
|
|
3112
|
-
# Uninstall from Claude Code (project-level)
|
|
3113
|
-
mcp-ticketer uninstall claude-code
|
|
3114
|
-
|
|
3115
|
-
# Uninstall from Claude Desktop (global)
|
|
3116
|
-
mcp-ticketer uninstall claude-desktop
|
|
3117
|
-
|
|
3118
|
-
# Uninstall from Auggie
|
|
3119
|
-
mcp-ticketer uninstall auggie
|
|
374
|
+
updates = {}
|
|
3120
375
|
|
|
3121
|
-
|
|
3122
|
-
|
|
376
|
+
# Set default adapter
|
|
377
|
+
if adapter:
|
|
378
|
+
updates["default_adapter"] = adapter.value
|
|
379
|
+
console.print(f"[green]ā[/green] Default adapter set to: {adapter.value}")
|
|
3123
380
|
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
remove(platform=platform, dry_run=dry_run)
|
|
381
|
+
# Build adapter-specific configuration
|
|
382
|
+
adapter_configs = {}
|
|
3127
383
|
|
|
384
|
+
# Linear configuration
|
|
385
|
+
if team_key or team_id:
|
|
386
|
+
linear_config = {}
|
|
387
|
+
if team_key:
|
|
388
|
+
linear_config["team_key"] = team_key
|
|
389
|
+
if team_id:
|
|
390
|
+
linear_config["team_id"] = team_id
|
|
391
|
+
adapter_configs["linear"] = linear_config
|
|
392
|
+
console.print("[green]ā[/green] Linear settings updated")
|
|
3128
393
|
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
394
|
+
# GitHub configuration
|
|
395
|
+
if owner or repo:
|
|
396
|
+
github_config = {}
|
|
397
|
+
if owner:
|
|
398
|
+
github_config["owner"] = owner
|
|
399
|
+
if repo:
|
|
400
|
+
github_config["repo"] = repo
|
|
401
|
+
adapter_configs["github"] = github_config
|
|
402
|
+
console.print("[green]ā[/green] GitHub settings updated")
|
|
3132
403
|
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
3143
|
-
raise typer.Exit(1) from None
|
|
3144
|
-
|
|
3145
|
-
# Display status
|
|
3146
|
-
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
3147
|
-
console.print(f"Operation: {item.operation}")
|
|
3148
|
-
console.print(f"Adapter: {item.adapter}")
|
|
3149
|
-
|
|
3150
|
-
# Status with color
|
|
3151
|
-
if item.status == QueueStatus.COMPLETED:
|
|
3152
|
-
console.print(f"Status: [green]{item.status}[/green]")
|
|
3153
|
-
elif item.status == QueueStatus.FAILED:
|
|
3154
|
-
console.print(f"Status: [red]{item.status}[/red]")
|
|
3155
|
-
elif item.status == QueueStatus.PROCESSING:
|
|
3156
|
-
console.print(f"Status: [yellow]{item.status}[/yellow]")
|
|
3157
|
-
else:
|
|
3158
|
-
console.print(f"Status: {item.status}")
|
|
404
|
+
# JIRA configuration
|
|
405
|
+
if server or project:
|
|
406
|
+
jira_config = {}
|
|
407
|
+
if server:
|
|
408
|
+
jira_config["server"] = server
|
|
409
|
+
if project:
|
|
410
|
+
jira_config["project_key"] = project
|
|
411
|
+
adapter_configs["jira"] = jira_config
|
|
412
|
+
console.print("[green]ā[/green] JIRA settings updated")
|
|
3159
413
|
|
|
3160
|
-
#
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
console.print(
|
|
414
|
+
# AITrackdown configuration
|
|
415
|
+
if base_path:
|
|
416
|
+
adapter_configs["aitrackdown"] = {"base_path": base_path}
|
|
417
|
+
console.print("[green]ā[/green] AITrackdown settings updated")
|
|
3164
418
|
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
console.print(f"\n[red]Error:[/red] {item.error_message}")
|
|
3168
|
-
elif item.result:
|
|
3169
|
-
console.print("\n[green]Result:[/green]")
|
|
3170
|
-
for key, value in item.result.items():
|
|
3171
|
-
console.print(f" {key}: {value}")
|
|
419
|
+
if adapter_configs:
|
|
420
|
+
updates["adapters"] = adapter_configs
|
|
3172
421
|
|
|
3173
|
-
|
|
3174
|
-
|
|
422
|
+
# Merge and save configuration
|
|
423
|
+
if updates:
|
|
424
|
+
config = merge_config(updates)
|
|
425
|
+
save_config(config)
|
|
426
|
+
console.print(f"[dim]Configuration saved to {CONFIG_FILE}[/dim]")
|
|
3175
427
|
|
|
3176
428
|
|
|
3177
|
-
@
|
|
3178
|
-
def
|
|
3179
|
-
|
|
3180
|
-
|
|
429
|
+
@app.command("configure")
|
|
430
|
+
def configure_command(
|
|
431
|
+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
432
|
+
adapter: str | None = typer.Option(
|
|
433
|
+
None, "--adapter", help="Set default adapter type"
|
|
3181
434
|
),
|
|
3182
|
-
|
|
3183
|
-
|
|
435
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
436
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
437
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
438
|
+
global_scope: bool = typer.Option(
|
|
439
|
+
False,
|
|
440
|
+
"--global",
|
|
441
|
+
"-g",
|
|
442
|
+
help="Save to global config instead of project-specific",
|
|
3184
443
|
),
|
|
3185
444
|
) -> None:
|
|
3186
|
-
"""
|
|
3187
|
-
|
|
3188
|
-
This command is used by Claude Code/Desktop when connecting to the MCP server.
|
|
3189
|
-
You typically don't need to run this manually - use 'mcp-ticketer install add' to configure.
|
|
3190
|
-
|
|
3191
|
-
Configuration Resolution:
|
|
3192
|
-
- When MCP server starts, it uses the current working directory (cwd)
|
|
3193
|
-
- The cwd is set by Claude Code/Desktop from the 'cwd' field in .mcp/config.json
|
|
3194
|
-
- Configuration is loaded with this priority:
|
|
3195
|
-
1. Project-specific: .mcp-ticketer/config.json in cwd
|
|
3196
|
-
2. Global: ~/.mcp-ticketer/config.json
|
|
3197
|
-
3. Default: aitrackdown adapter with .aitrackdown base path
|
|
3198
|
-
"""
|
|
3199
|
-
from ..mcp.server.server_sdk import configure_adapter
|
|
3200
|
-
from ..mcp.server.server_sdk import main as sdk_main
|
|
3201
|
-
|
|
3202
|
-
# Load configuration (respects project-specific config in cwd)
|
|
3203
|
-
config = load_config()
|
|
3204
|
-
|
|
3205
|
-
# Determine adapter type with priority: CLI arg > config > .env files > default
|
|
3206
|
-
if adapter:
|
|
3207
|
-
# Priority 1: Command line argument
|
|
3208
|
-
adapter_type = adapter.value
|
|
3209
|
-
# Get base config from config file
|
|
3210
|
-
adapters_config = config.get("adapters", {})
|
|
3211
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
3212
|
-
else:
|
|
3213
|
-
# Priority 2: Configuration file (project-specific)
|
|
3214
|
-
adapter_type = config.get("default_adapter")
|
|
3215
|
-
if adapter_type:
|
|
3216
|
-
adapters_config = config.get("adapters", {})
|
|
3217
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
3218
|
-
else:
|
|
3219
|
-
# Priority 3: .env files (auto-detection fallback)
|
|
3220
|
-
from ..mcp.server.main import _load_env_configuration
|
|
3221
|
-
|
|
3222
|
-
env_config = _load_env_configuration()
|
|
3223
|
-
if env_config:
|
|
3224
|
-
adapter_type = env_config["adapter_type"]
|
|
3225
|
-
adapter_config = env_config["adapter_config"]
|
|
3226
|
-
else:
|
|
3227
|
-
# Priority 4: Default fallback
|
|
3228
|
-
adapter_type = "aitrackdown"
|
|
3229
|
-
adapters_config = config.get("adapters", {})
|
|
3230
|
-
adapter_config = adapters_config.get(adapter_type, {})
|
|
3231
|
-
|
|
3232
|
-
# Override with command line options if provided (highest priority)
|
|
3233
|
-
if base_path and adapter_type == "aitrackdown":
|
|
3234
|
-
adapter_config["base_path"] = base_path
|
|
3235
|
-
|
|
3236
|
-
# Fallback to legacy config format
|
|
3237
|
-
if not adapter_config and "config" in config:
|
|
3238
|
-
adapter_config = config["config"]
|
|
445
|
+
"""Configure MCP Ticketer integration.
|
|
3239
446
|
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
447
|
+
Run without arguments to launch interactive wizard.
|
|
448
|
+
Use --show to display current configuration.
|
|
449
|
+
Use options to set specific values directly.
|
|
450
|
+
"""
|
|
451
|
+
# Show configuration
|
|
452
|
+
if show:
|
|
453
|
+
show_current_config()
|
|
454
|
+
return
|
|
3243
455
|
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
456
|
+
# Direct configuration
|
|
457
|
+
if any([adapter, api_key, project_id, team_id]):
|
|
458
|
+
set_adapter_config(
|
|
459
|
+
adapter=adapter,
|
|
460
|
+
api_key=api_key,
|
|
461
|
+
project_id=project_id,
|
|
462
|
+
team_id=team_id,
|
|
463
|
+
global_scope=global_scope,
|
|
3252
464
|
)
|
|
465
|
+
return
|
|
3253
466
|
|
|
3254
|
-
#
|
|
3255
|
-
|
|
3256
|
-
configure_adapter(adapter_type, adapter_config)
|
|
3257
|
-
sdk_main()
|
|
3258
|
-
except KeyboardInterrupt:
|
|
3259
|
-
# Send this to stderr
|
|
3260
|
-
if sys.stderr.isatty():
|
|
3261
|
-
console.print("\n[yellow]Server stopped by user[/yellow]")
|
|
3262
|
-
sys.exit(0)
|
|
3263
|
-
except Exception as e:
|
|
3264
|
-
# Log error to stderr
|
|
3265
|
-
sys.stderr.write(f"MCP server error: {e}\n")
|
|
3266
|
-
sys.exit(1)
|
|
467
|
+
# Run interactive wizard
|
|
468
|
+
configure_wizard()
|
|
3267
469
|
|
|
3268
470
|
|
|
3269
|
-
@
|
|
3270
|
-
def
|
|
3271
|
-
|
|
471
|
+
@app.command("config")
|
|
472
|
+
def config_alias(
|
|
473
|
+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
|
|
474
|
+
adapter: str | None = typer.Option(
|
|
475
|
+
None, "--adapter", help="Set default adapter type"
|
|
476
|
+
),
|
|
477
|
+
api_key: str | None = typer.Option(None, "--api-key", help="Set API key/token"),
|
|
478
|
+
project_id: str | None = typer.Option(None, "--project-id", help="Set project ID"),
|
|
479
|
+
team_id: str | None = typer.Option(None, "--team-id", help="Set team ID (Linear)"),
|
|
480
|
+
global_scope: bool = typer.Option(
|
|
3272
481
|
False,
|
|
3273
482
|
"--global",
|
|
3274
483
|
"-g",
|
|
3275
|
-
help="
|
|
3276
|
-
),
|
|
3277
|
-
force: bool = typer.Option(
|
|
3278
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
484
|
+
help="Save to global config instead of project-specific",
|
|
3279
485
|
),
|
|
3280
486
|
) -> None:
|
|
3281
|
-
"""
|
|
3282
|
-
|
|
3283
|
-
Reads configuration from .mcp-ticketer/config.json and updates
|
|
3284
|
-
Claude Code's MCP settings accordingly.
|
|
3285
|
-
|
|
3286
|
-
By default, configures project-level (.mcp/config.json).
|
|
3287
|
-
Use --global to configure Claude Desktop instead.
|
|
3288
|
-
|
|
3289
|
-
Examples:
|
|
3290
|
-
# Configure for current project (default)
|
|
3291
|
-
mcp-ticketer mcp claude
|
|
3292
|
-
|
|
3293
|
-
# Configure Claude Desktop globally
|
|
3294
|
-
mcp-ticketer mcp claude --global
|
|
3295
|
-
|
|
3296
|
-
# Force overwrite existing configuration
|
|
3297
|
-
mcp-ticketer mcp claude --force
|
|
487
|
+
"""Alias for configure command - shorter syntax."""
|
|
488
|
+
configure_command(show, adapter, api_key, project_id, team_id, global_scope)
|
|
3298
489
|
|
|
3299
|
-
"""
|
|
3300
|
-
from ..cli.mcp_configure import configure_claude_mcp
|
|
3301
|
-
|
|
3302
|
-
try:
|
|
3303
|
-
configure_claude_mcp(global_config=global_config, force=force)
|
|
3304
|
-
except Exception as e:
|
|
3305
|
-
console.print(f"[red]ā Configuration failed:[/red] {e}")
|
|
3306
|
-
raise typer.Exit(1) from e
|
|
3307
490
|
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
"project",
|
|
3313
|
-
"--scope",
|
|
3314
|
-
"-s",
|
|
3315
|
-
help="Configuration scope: 'project' (default) or 'user'",
|
|
3316
|
-
),
|
|
3317
|
-
force: bool = typer.Option(
|
|
3318
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
491
|
+
@app.command("migrate-config")
|
|
492
|
+
def migrate_config(
|
|
493
|
+
dry_run: bool = typer.Option(
|
|
494
|
+
False, "--dry-run", help="Show what would be done without making changes"
|
|
3319
495
|
),
|
|
3320
496
|
) -> None:
|
|
3321
|
-
"""
|
|
3322
|
-
|
|
3323
|
-
Reads configuration from .mcp-ticketer/config.json and creates
|
|
3324
|
-
Gemini CLI settings file with mcp-ticketer configuration.
|
|
3325
|
-
|
|
3326
|
-
By default, configures project-level (.gemini/settings.json).
|
|
3327
|
-
Use --scope user to configure user-level (~/.gemini/settings.json).
|
|
3328
|
-
|
|
3329
|
-
Examples:
|
|
3330
|
-
# Configure for current project (default)
|
|
3331
|
-
mcp-ticketer mcp gemini
|
|
3332
|
-
|
|
3333
|
-
# Configure at user level
|
|
3334
|
-
mcp-ticketer mcp gemini --scope user
|
|
3335
|
-
|
|
3336
|
-
# Force overwrite existing configuration
|
|
3337
|
-
mcp-ticketer mcp gemini --force
|
|
497
|
+
"""Migrate configuration from old format to new format.
|
|
3338
498
|
|
|
499
|
+
This command will:
|
|
500
|
+
1. Detect old configuration format
|
|
501
|
+
2. Convert to new schema
|
|
502
|
+
3. Backup old config
|
|
503
|
+
4. Apply new config
|
|
3339
504
|
"""
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
# Validate scope parameter
|
|
3343
|
-
if scope not in ["project", "user"]:
|
|
3344
|
-
console.print(
|
|
3345
|
-
f"[red]ā Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
|
|
3346
|
-
)
|
|
3347
|
-
raise typer.Exit(1) from None
|
|
505
|
+
migrate_config_command(dry_run=dry_run)
|
|
3348
506
|
|
|
3349
|
-
try:
|
|
3350
|
-
configure_gemini_mcp(scope=scope, force=force) # type: ignore
|
|
3351
|
-
except Exception as e:
|
|
3352
|
-
console.print(f"[red]ā Configuration failed:[/red] {e}")
|
|
3353
|
-
raise typer.Exit(1) from e
|
|
3354
507
|
|
|
508
|
+
# Add ticket command group to main app
|
|
509
|
+
app.add_typer(ticket_app, name="ticket")
|
|
3355
510
|
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
force: bool = typer.Option(
|
|
3359
|
-
False, "--force", "-f", help="Overwrite existing configuration"
|
|
3360
|
-
),
|
|
3361
|
-
) -> None:
|
|
3362
|
-
"""Configure Codex CLI to use mcp-ticketer MCP server.
|
|
511
|
+
# Add platform command group to main app
|
|
512
|
+
app.add_typer(platform_app, name="platform")
|
|
3363
513
|
|
|
3364
|
-
|
|
3365
|
-
|
|
514
|
+
# Add queue command to main app
|
|
515
|
+
app.add_typer(queue_app, name="queue")
|
|
3366
516
|
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
you must restart Codex CLI for changes to take effect.
|
|
517
|
+
# Add discover command to main app
|
|
518
|
+
app.add_typer(discover_app, name="discover")
|
|
3370
519
|
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
mcp-ticketer mcp codex
|
|
520
|
+
# Add instructions command to main app
|
|
521
|
+
app.add_typer(instruction_app, name="instructions")
|
|
3374
522
|
|
|
3375
|
-
|
|
3376
|
-
|
|
523
|
+
# Add project-update command group to main app
|
|
524
|
+
app.add_typer(project_update_app, name="project-update")
|
|
3377
525
|
|
|
3378
|
-
|
|
3379
|
-
|
|
526
|
+
# Add setup and init commands to main app
|
|
527
|
+
app.command()(setup)
|
|
528
|
+
app.command()(init)
|
|
3380
529
|
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
raise typer.Exit(1) from e
|
|
530
|
+
# Add platform installer commands to main app
|
|
531
|
+
app.command()(install)
|
|
532
|
+
app.command()(remove)
|
|
533
|
+
app.command()(uninstall)
|
|
3386
534
|
|
|
3387
535
|
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
536
|
+
# Add diagnostics command
|
|
537
|
+
@app.command("doctor")
|
|
538
|
+
def doctor_command(
|
|
539
|
+
output_file: str | None = typer.Option(
|
|
540
|
+
None, "--output", "-o", help="Save full report to file"
|
|
541
|
+
),
|
|
542
|
+
json_output: bool = typer.Option(
|
|
543
|
+
False, "--json", help="Output report in JSON format"
|
|
544
|
+
),
|
|
545
|
+
simple: bool = typer.Option(
|
|
546
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
3392
547
|
),
|
|
3393
548
|
) -> None:
|
|
3394
|
-
"""
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
Auggie CLI settings.json with mcp-ticketer configuration.
|
|
3398
|
-
|
|
3399
|
-
IMPORTANT: Auggie CLI ONLY supports global configuration at ~/.augment/settings.json.
|
|
3400
|
-
There is no project-level configuration support. After configuration,
|
|
3401
|
-
you must restart Auggie CLI for changes to take effect.
|
|
3402
|
-
|
|
3403
|
-
Examples:
|
|
3404
|
-
# Configure Auggie CLI globally
|
|
3405
|
-
mcp-ticketer mcp auggie
|
|
3406
|
-
|
|
3407
|
-
# Force overwrite existing configuration
|
|
3408
|
-
mcp-ticketer mcp auggie --force
|
|
3409
|
-
|
|
3410
|
-
"""
|
|
3411
|
-
from ..cli.auggie_configure import configure_auggie_mcp
|
|
3412
|
-
|
|
3413
|
-
try:
|
|
3414
|
-
configure_auggie_mcp(force=force)
|
|
3415
|
-
except Exception as e:
|
|
3416
|
-
console.print(f"[red]ā Configuration failed:[/red] {e}")
|
|
3417
|
-
raise typer.Exit(1) from e
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
@mcp_app.command(name="status")
|
|
3421
|
-
def mcp_status() -> None:
|
|
3422
|
-
"""Check MCP server status.
|
|
3423
|
-
|
|
3424
|
-
Shows whether the MCP server is configured and running for various platforms.
|
|
3425
|
-
|
|
3426
|
-
Examples:
|
|
3427
|
-
mcp-ticketer mcp status
|
|
3428
|
-
|
|
3429
|
-
"""
|
|
3430
|
-
import json
|
|
3431
|
-
from pathlib import Path
|
|
549
|
+
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
550
|
+
if simple:
|
|
551
|
+
from .simple_health import simple_diagnose
|
|
3432
552
|
|
|
3433
|
-
|
|
553
|
+
report = simple_diagnose()
|
|
554
|
+
if output_file:
|
|
555
|
+
import json
|
|
3434
556
|
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
with open(project_config) as f:
|
|
3441
|
-
config = json.load(f)
|
|
3442
|
-
adapter = config.get("default_adapter", "aitrackdown")
|
|
3443
|
-
console.print(f" Default adapter: [cyan]{adapter}[/cyan]")
|
|
3444
|
-
except Exception as e:
|
|
3445
|
-
console.print(f" [yellow]Warning: Could not read config: {e}[/yellow]")
|
|
3446
|
-
else:
|
|
3447
|
-
console.print("[yellow]ā[/yellow] No project config found")
|
|
557
|
+
with open(output_file, "w") as f:
|
|
558
|
+
json.dump(report, f, indent=2)
|
|
559
|
+
console.print(f"\nš Report saved to: {output_file}")
|
|
560
|
+
if json_output:
|
|
561
|
+
import json
|
|
3448
562
|
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
console.print(
|
|
3453
|
-
f"\n[green]ā[/green] Claude Code configured: {claude_code_config}"
|
|
3454
|
-
)
|
|
563
|
+
console.print("\n" + json.dumps(report, indent=2))
|
|
564
|
+
if report["issues"]:
|
|
565
|
+
raise typer.Exit(1) from None
|
|
3455
566
|
else:
|
|
3456
|
-
console.print("\n[yellow]ā[/yellow] Claude Code not configured")
|
|
3457
|
-
|
|
3458
|
-
# Check Claude Desktop configuration
|
|
3459
|
-
claude_desktop_config = (
|
|
3460
|
-
Path.home()
|
|
3461
|
-
/ "Library"
|
|
3462
|
-
/ "Application Support"
|
|
3463
|
-
/ "Claude"
|
|
3464
|
-
/ "claude_desktop_config.json"
|
|
3465
|
-
)
|
|
3466
|
-
if claude_desktop_config.exists():
|
|
3467
567
|
try:
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
if "mcpServers" in config and "mcp-ticketer" in config["mcpServers"]:
|
|
3471
|
-
console.print(
|
|
3472
|
-
f"[green]ā[/green] Claude Desktop configured: {claude_desktop_config}"
|
|
3473
|
-
)
|
|
3474
|
-
else:
|
|
3475
|
-
console.print(
|
|
3476
|
-
"[yellow]ā[/yellow] Claude Desktop config exists but mcp-ticketer not found"
|
|
3477
|
-
)
|
|
3478
|
-
except Exception:
|
|
3479
|
-
console.print(
|
|
3480
|
-
"[yellow]ā[/yellow] Claude Desktop config exists but could not be read"
|
|
568
|
+
asyncio.run(
|
|
569
|
+
run_diagnostics(output_file=output_file, json_output=json_output)
|
|
3481
570
|
)
|
|
3482
|
-
|
|
3483
|
-
|
|
571
|
+
except typer.Exit:
|
|
572
|
+
# typer.Exit is expected - don't fall back to simple diagnostics
|
|
573
|
+
raise
|
|
574
|
+
except Exception as e:
|
|
575
|
+
console.print(f"ā ļø Full diagnostics failed: {e}")
|
|
576
|
+
console.print("š Falling back to simple diagnostics...")
|
|
577
|
+
from .simple_health import simple_diagnose
|
|
3484
578
|
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
if gemini_project_config.exists():
|
|
3489
|
-
console.print(
|
|
3490
|
-
f"\n[green]ā[/green] Gemini (project) configured: {gemini_project_config}"
|
|
3491
|
-
)
|
|
3492
|
-
elif gemini_user_config.exists():
|
|
3493
|
-
console.print(
|
|
3494
|
-
f"\n[green]ā[/green] Gemini (user) configured: {gemini_user_config}"
|
|
3495
|
-
)
|
|
3496
|
-
else:
|
|
3497
|
-
console.print("\n[yellow]ā[/yellow] Gemini not configured")
|
|
579
|
+
report = simple_diagnose()
|
|
580
|
+
if report["issues"]:
|
|
581
|
+
raise typer.Exit(1) from None
|
|
3498
582
|
|
|
3499
|
-
# Check Codex configuration
|
|
3500
|
-
codex_config = Path.home() / ".codex" / "config.toml"
|
|
3501
|
-
if codex_config.exists():
|
|
3502
|
-
console.print(f"[green]ā[/green] Codex configured: {codex_config}")
|
|
3503
|
-
else:
|
|
3504
|
-
console.print("[yellow]ā[/yellow] Codex not configured")
|
|
3505
583
|
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
584
|
+
@app.command("diagnose", hidden=True)
|
|
585
|
+
def diagnose_alias(
|
|
586
|
+
output_file: str | None = typer.Option(
|
|
587
|
+
None, "--output", "-o", help="Save full report to file"
|
|
588
|
+
),
|
|
589
|
+
json_output: bool = typer.Option(
|
|
590
|
+
False, "--json", help="Output report in JSON format"
|
|
591
|
+
),
|
|
592
|
+
simple: bool = typer.Option(
|
|
593
|
+
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
594
|
+
),
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Run comprehensive system diagnostics and health check (alias for doctor)."""
|
|
597
|
+
# Call the doctor_command function with the same parameters
|
|
598
|
+
doctor_command(output_file=output_file, json_output=json_output, simple=simple)
|
|
3512
599
|
|
|
3513
|
-
console.print(
|
|
3514
|
-
"\n[dim]Run 'mcp-ticketer install <platform>' to configure a platform[/dim]"
|
|
3515
|
-
)
|
|
3516
600
|
|
|
601
|
+
@app.command("status")
|
|
602
|
+
def status_command() -> None:
|
|
603
|
+
"""Quick health check - shows system status summary (alias: health)."""
|
|
604
|
+
from .simple_health import simple_health_check
|
|
3517
605
|
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
606
|
+
result = simple_health_check()
|
|
607
|
+
if result != 0:
|
|
608
|
+
raise typer.Exit(result) from None
|
|
3521
609
|
|
|
3522
|
-
Note: The MCP server runs on-demand when AI clients connect via stdio.
|
|
3523
|
-
It doesn't run as a persistent background service, so there's nothing to stop.
|
|
3524
|
-
This command is provided for consistency but has no effect.
|
|
3525
610
|
|
|
3526
|
-
|
|
3527
|
-
|
|
611
|
+
@app.command("health")
|
|
612
|
+
def health_alias() -> None:
|
|
613
|
+
"""Quick health check - shows system status summary (alias for status)."""
|
|
614
|
+
from .simple_health import simple_health_check
|
|
3528
615
|
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
)
|
|
3533
|
-
console.print("There is no persistent server process to stop.")
|
|
3534
|
-
console.print(
|
|
3535
|
-
"\n[dim]The server starts automatically when AI clients connect and stops when they disconnect.[/dim]"
|
|
3536
|
-
)
|
|
616
|
+
result = simple_health_check()
|
|
617
|
+
if result != 0:
|
|
618
|
+
raise typer.Exit(result) from None
|
|
3537
619
|
|
|
3538
620
|
|
|
3539
621
|
# Add command groups to main app (must be after all subcommands are defined)
|