mcp-ticketer 0.4.11__py3-none-any.whl → 0.12.0__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/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +9 -3
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1308 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +334 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +313 -96
- mcp_ticketer/adapters/jira.py +251 -1
- mcp_ticketer/adapters/linear/adapter.py +524 -22
- mcp_ticketer/adapters/linear/client.py +61 -9
- mcp_ticketer/adapters/linear/mappers.py +9 -3
- mcp_ticketer/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +1 -1
- mcp_ticketer/cli/codex_configure.py +80 -1
- mcp_ticketer/cli/configure.py +33 -43
- mcp_ticketer/cli/diagnostics.py +18 -16
- mcp_ticketer/cli/discover.py +288 -21
- mcp_ticketer/cli/gemini_configure.py +1 -1
- mcp_ticketer/cli/instruction_commands.py +429 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +1199 -227
- mcp_ticketer/cli/mcp_configure.py +1 -1
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +412 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/simple_health.py +1 -1
- mcp_ticketer/cli/ticket_commands.py +14 -13
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +12 -0
- mcp_ticketer/core/adapter.py +4 -4
- mcp_ticketer/core/config.py +17 -10
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +3 -3
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/mappers.py +1 -1
- mcp_ticketer/core/models.py +1 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +17 -1
- mcp_ticketer/core/registry.py +1 -1
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/main.py +82 -69
- mcp_ticketer/mcp/server/tools/__init__.py +9 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
- mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
- mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +14 -12
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
- mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
mcp_ticketer/cli/main.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
from enum import Enum
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
8
9
|
|
|
9
10
|
import typer
|
|
10
11
|
from dotenv import load_dotenv
|
|
@@ -23,6 +24,7 @@ from ..queue.ticket_registry import TicketRegistry
|
|
|
23
24
|
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
24
25
|
from .diagnostics import run_diagnostics
|
|
25
26
|
from .discover import app as discover_app
|
|
27
|
+
from .instruction_commands import app as instruction_app
|
|
26
28
|
from .migrate_config import migrate_config_command
|
|
27
29
|
from .platform_commands import app as platform_app
|
|
28
30
|
from .queue_commands import app as queue_app
|
|
@@ -48,11 +50,11 @@ app = typer.Typer(
|
|
|
48
50
|
console = Console()
|
|
49
51
|
|
|
50
52
|
|
|
51
|
-
def version_callback(value: bool):
|
|
53
|
+
def version_callback(value: bool) -> None:
|
|
52
54
|
"""Print version and exit."""
|
|
53
55
|
if value:
|
|
54
56
|
console.print(f"mcp-ticketer version {__version__}")
|
|
55
|
-
raise typer.Exit()
|
|
57
|
+
raise typer.Exit() from None
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
@app.callback()
|
|
@@ -65,7 +67,7 @@ def main_callback(
|
|
|
65
67
|
is_eager=True,
|
|
66
68
|
help="Show version and exit",
|
|
67
69
|
),
|
|
68
|
-
):
|
|
70
|
+
) -> None:
|
|
69
71
|
"""MCP Ticketer - Universal ticket management interface."""
|
|
70
72
|
pass
|
|
71
73
|
|
|
@@ -270,7 +272,7 @@ def merge_config(updates: dict) -> dict:
|
|
|
270
272
|
|
|
271
273
|
def get_adapter(
|
|
272
274
|
override_adapter: str | None = None, override_config: dict | None = None
|
|
273
|
-
):
|
|
275
|
+
) -> Any:
|
|
274
276
|
"""Get configured adapter instance.
|
|
275
277
|
|
|
276
278
|
Args:
|
|
@@ -318,6 +320,416 @@ def get_adapter(
|
|
|
318
320
|
return AdapterRegistry.get_adapter(adapter_type, adapter_config)
|
|
319
321
|
|
|
320
322
|
|
|
323
|
+
async def _validate_adapter_credentials(
|
|
324
|
+
adapter_type: str, config_file_path: Path
|
|
325
|
+
) -> list[str]:
|
|
326
|
+
"""Validate adapter credentials by performing real connectivity tests.
|
|
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
|
+
|
|
321
733
|
def _prompt_for_adapter_selection(console: Console) -> str:
|
|
322
734
|
"""Interactive prompt for adapter selection.
|
|
323
735
|
|
|
@@ -381,90 +793,354 @@ def _prompt_for_adapter_selection(console: Console) -> str:
|
|
|
381
793
|
)
|
|
382
794
|
except (ValueError, typer.Abort):
|
|
383
795
|
console.print("[yellow]Setup cancelled.[/yellow]")
|
|
384
|
-
raise typer.Exit(0)
|
|
796
|
+
raise typer.Exit(0) from None
|
|
385
797
|
|
|
386
798
|
|
|
387
799
|
@app.command()
|
|
388
800
|
def setup(
|
|
389
|
-
adapter: str | None = typer.Option(
|
|
390
|
-
None,
|
|
391
|
-
"--adapter",
|
|
392
|
-
"-a",
|
|
393
|
-
help="Adapter type to use (interactive prompt if not specified)",
|
|
394
|
-
),
|
|
395
801
|
project_path: str | None = typer.Option(
|
|
396
802
|
None, "--path", help="Project path (default: current directory)"
|
|
397
803
|
),
|
|
398
|
-
|
|
804
|
+
skip_platforms: bool = typer.Option(
|
|
399
805
|
False,
|
|
400
|
-
"--
|
|
401
|
-
"
|
|
402
|
-
help="Save to global config instead of project-specific",
|
|
806
|
+
"--skip-platforms",
|
|
807
|
+
help="Skip platform installation (only initialize adapter)",
|
|
403
808
|
),
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
"--
|
|
407
|
-
"-
|
|
408
|
-
help="Base path for ticket storage (AITrackdown only)",
|
|
409
|
-
),
|
|
410
|
-
api_key: str | None = typer.Option(
|
|
411
|
-
None, "--api-key", help="API key for Linear or API token for JIRA"
|
|
412
|
-
),
|
|
413
|
-
team_id: str | None = typer.Option(
|
|
414
|
-
None, "--team-id", help="Linear team ID (required for Linear adapter)"
|
|
415
|
-
),
|
|
416
|
-
jira_server: str | None = typer.Option(
|
|
417
|
-
None,
|
|
418
|
-
"--jira-server",
|
|
419
|
-
help="JIRA server URL (e.g., https://company.atlassian.net)",
|
|
420
|
-
),
|
|
421
|
-
jira_email: str | None = typer.Option(
|
|
422
|
-
None, "--jira-email", help="JIRA user email for authentication"
|
|
423
|
-
),
|
|
424
|
-
jira_project: str | None = typer.Option(
|
|
425
|
-
None, "--jira-project", help="Default JIRA project key"
|
|
426
|
-
),
|
|
427
|
-
github_owner: str | None = typer.Option(
|
|
428
|
-
None, "--github-owner", help="GitHub repository owner"
|
|
429
|
-
),
|
|
430
|
-
github_repo: str | None = typer.Option(
|
|
431
|
-
None, "--github-repo", help="GitHub repository name"
|
|
432
|
-
),
|
|
433
|
-
github_token: str | None = typer.Option(
|
|
434
|
-
None, "--github-token", help="GitHub Personal Access Token"
|
|
809
|
+
force_reinit: bool = typer.Option(
|
|
810
|
+
False,
|
|
811
|
+
"--force-reinit",
|
|
812
|
+
help="Force re-initialization even if config exists",
|
|
435
813
|
),
|
|
436
814
|
) -> None:
|
|
437
|
-
"""
|
|
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.
|
|
438
819
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
442
825
|
|
|
443
826
|
Examples:
|
|
444
|
-
#
|
|
827
|
+
# Smart setup (recommended for first-time setup)
|
|
445
828
|
mcp-ticketer setup
|
|
446
829
|
|
|
447
|
-
# Setup with specific adapter
|
|
448
|
-
mcp-ticketer setup --adapter linear
|
|
449
|
-
|
|
450
830
|
# Setup for different project
|
|
451
831
|
mcp-ticketer setup --path /path/to/project
|
|
452
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
|
+
|
|
453
841
|
"""
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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]"
|
|
468
1144
|
)
|
|
469
1145
|
|
|
470
1146
|
|
|
@@ -518,37 +1194,41 @@ def init(
|
|
|
518
1194
|
None, "--github-token", help="GitHub Personal Access Token"
|
|
519
1195
|
),
|
|
520
1196
|
) -> None:
|
|
521
|
-
"""Initialize
|
|
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.
|
|
522
1202
|
|
|
523
|
-
|
|
524
|
-
to guide you through the process. It auto-detects adapter configuration
|
|
525
|
-
from .env files or prompts for interactive setup if no configuration is found.
|
|
1203
|
+
Creates .mcp-ticketer/config.json in the current directory.
|
|
526
1204
|
|
|
527
|
-
|
|
528
|
-
|
|
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.
|
|
529
1208
|
|
|
530
|
-
|
|
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'
|
|
531
1214
|
|
|
532
1215
|
Examples:
|
|
533
|
-
#
|
|
534
|
-
mcp-ticketer init
|
|
535
|
-
mcp-ticketer install
|
|
1216
|
+
# For first-time setup, use 'setup' instead (recommended)
|
|
536
1217
|
mcp-ticketer setup
|
|
537
1218
|
|
|
1219
|
+
# Initialize adapter only (advanced usage)
|
|
1220
|
+
mcp-ticketer init
|
|
1221
|
+
|
|
538
1222
|
# Force specific adapter
|
|
539
1223
|
mcp-ticketer init --adapter linear
|
|
540
1224
|
|
|
541
1225
|
# Initialize for different project
|
|
542
1226
|
mcp-ticketer init --path /path/to/project
|
|
543
1227
|
|
|
544
|
-
# Save globally (not recommended)
|
|
545
|
-
mcp-ticketer init --global
|
|
546
|
-
|
|
547
1228
|
"""
|
|
548
1229
|
from pathlib import Path
|
|
549
1230
|
|
|
550
1231
|
from ..core.env_discovery import discover_config
|
|
551
|
-
from ..core.project_config import ConfigResolver
|
|
552
1232
|
|
|
553
1233
|
# Determine project path
|
|
554
1234
|
proj_path = Path(project_path) if project_path else Path.cwd()
|
|
@@ -563,7 +1243,7 @@ def init(
|
|
|
563
1243
|
default=False,
|
|
564
1244
|
):
|
|
565
1245
|
console.print("[yellow]Initialization cancelled.[/yellow]")
|
|
566
|
-
raise typer.Exit(0)
|
|
1246
|
+
raise typer.Exit(0) from None
|
|
567
1247
|
|
|
568
1248
|
# 1. Try auto-discovery if no adapter specified
|
|
569
1249
|
discovered = None
|
|
@@ -652,11 +1332,9 @@ def init(
|
|
|
652
1332
|
elif adapter_type == "linear":
|
|
653
1333
|
# If not auto-discovered, build from CLI params or prompt
|
|
654
1334
|
if adapter_type not in config["adapters"]:
|
|
655
|
-
linear_config = {}
|
|
656
|
-
|
|
657
1335
|
# API Key
|
|
658
1336
|
linear_api_key = api_key or os.getenv("LINEAR_API_KEY")
|
|
659
|
-
if not linear_api_key
|
|
1337
|
+
if not linear_api_key:
|
|
660
1338
|
console.print("\n[bold]Linear Configuration[/bold]")
|
|
661
1339
|
console.print("You need a Linear API key to connect to Linear.")
|
|
662
1340
|
console.print(
|
|
@@ -667,22 +1345,74 @@ def init(
|
|
|
667
1345
|
"Enter your Linear API key", hide_input=True
|
|
668
1346
|
)
|
|
669
1347
|
|
|
670
|
-
|
|
671
|
-
linear_config["api_key"] = linear_api_key
|
|
672
|
-
|
|
673
|
-
# Team ID or Team Key
|
|
1348
|
+
# Team ID or Team Key or Team URL
|
|
674
1349
|
# Try environment variables first
|
|
675
1350
|
linear_team_key = os.getenv("LINEAR_TEAM_KEY")
|
|
676
1351
|
linear_team_id = team_id or os.getenv("LINEAR_TEAM_ID")
|
|
677
1352
|
|
|
678
|
-
if not linear_team_key and not linear_team_id
|
|
1353
|
+
if not linear_team_key and not linear_team_id:
|
|
679
1354
|
console.print("\n[bold]Linear Team Configuration[/bold]")
|
|
680
|
-
console.print("
|
|
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)")
|
|
681
1361
|
console.print(
|
|
682
|
-
"[dim]Find
|
|
1362
|
+
"[dim]Find team URL or key in: Linear → Your Team → Team Issues Page[/dim]\n"
|
|
683
1363
|
)
|
|
684
1364
|
|
|
685
|
-
|
|
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
|
+
}
|
|
686
1416
|
|
|
687
1417
|
# Save whichever was provided
|
|
688
1418
|
if linear_team_key:
|
|
@@ -690,18 +1420,6 @@ def init(
|
|
|
690
1420
|
if linear_team_id:
|
|
691
1421
|
linear_config["team_id"] = linear_team_id
|
|
692
1422
|
|
|
693
|
-
if not linear_config.get("api_key") or (
|
|
694
|
-
not linear_config.get("team_id") and not linear_config.get("team_key")
|
|
695
|
-
):
|
|
696
|
-
console.print(
|
|
697
|
-
"[red]Error:[/red] Linear requires both API key and team ID/key"
|
|
698
|
-
)
|
|
699
|
-
console.print(
|
|
700
|
-
"Run 'mcp-ticketer init --adapter linear' with proper credentials"
|
|
701
|
-
)
|
|
702
|
-
raise typer.Exit(1)
|
|
703
|
-
|
|
704
|
-
linear_config["type"] = "linear"
|
|
705
1423
|
config["adapters"]["linear"] = linear_config
|
|
706
1424
|
|
|
707
1425
|
elif adapter_type == "jira":
|
|
@@ -713,7 +1431,7 @@ def init(
|
|
|
713
1431
|
project = jira_project or os.getenv("JIRA_PROJECT_KEY")
|
|
714
1432
|
|
|
715
1433
|
# Interactive prompts for missing values
|
|
716
|
-
if not server
|
|
1434
|
+
if not server:
|
|
717
1435
|
console.print("\n[bold]JIRA Configuration[/bold]")
|
|
718
1436
|
console.print("Enter your JIRA server details.\n")
|
|
719
1437
|
|
|
@@ -721,10 +1439,10 @@ def init(
|
|
|
721
1439
|
"JIRA server URL (e.g., https://company.atlassian.net)"
|
|
722
1440
|
)
|
|
723
1441
|
|
|
724
|
-
if not email
|
|
1442
|
+
if not email:
|
|
725
1443
|
email = typer.prompt("Your JIRA email address")
|
|
726
1444
|
|
|
727
|
-
if not token
|
|
1445
|
+
if not token:
|
|
728
1446
|
console.print("\nYou need a JIRA API token.")
|
|
729
1447
|
console.print(
|
|
730
1448
|
"[dim]Generate one at: https://id.atlassian.com/manage/api-tokens[/dim]\n"
|
|
@@ -732,7 +1450,7 @@ def init(
|
|
|
732
1450
|
|
|
733
1451
|
token = typer.prompt("Enter your JIRA API token", hide_input=True)
|
|
734
1452
|
|
|
735
|
-
if not project
|
|
1453
|
+
if not project:
|
|
736
1454
|
project = typer.prompt(
|
|
737
1455
|
"Default JIRA project key (optional, press Enter to skip)",
|
|
738
1456
|
default="",
|
|
@@ -742,15 +1460,15 @@ def init(
|
|
|
742
1460
|
# Validate required fields
|
|
743
1461
|
if not server:
|
|
744
1462
|
console.print("[red]Error:[/red] JIRA server URL is required")
|
|
745
|
-
raise typer.Exit(1)
|
|
1463
|
+
raise typer.Exit(1) from None
|
|
746
1464
|
|
|
747
1465
|
if not email:
|
|
748
1466
|
console.print("[red]Error:[/red] JIRA email is required")
|
|
749
|
-
raise typer.Exit(1)
|
|
1467
|
+
raise typer.Exit(1) from None
|
|
750
1468
|
|
|
751
1469
|
if not token:
|
|
752
1470
|
console.print("[red]Error:[/red] JIRA API token is required")
|
|
753
|
-
raise typer.Exit(1)
|
|
1471
|
+
raise typer.Exit(1) from None
|
|
754
1472
|
|
|
755
1473
|
jira_config = {
|
|
756
1474
|
"server": server,
|
|
@@ -772,7 +1490,7 @@ def init(
|
|
|
772
1490
|
token = github_token or os.getenv("GITHUB_TOKEN")
|
|
773
1491
|
|
|
774
1492
|
# Interactive prompts for missing values
|
|
775
|
-
if not owner
|
|
1493
|
+
if not owner:
|
|
776
1494
|
console.print("\n[bold]GitHub Configuration[/bold]")
|
|
777
1495
|
console.print("Enter your GitHub repository details.\n")
|
|
778
1496
|
|
|
@@ -780,10 +1498,10 @@ def init(
|
|
|
780
1498
|
"GitHub repository owner (username or organization)"
|
|
781
1499
|
)
|
|
782
1500
|
|
|
783
|
-
if not repo
|
|
1501
|
+
if not repo:
|
|
784
1502
|
repo = typer.prompt("GitHub repository name")
|
|
785
1503
|
|
|
786
|
-
if not token
|
|
1504
|
+
if not token:
|
|
787
1505
|
console.print("\nYou need a GitHub Personal Access Token.")
|
|
788
1506
|
console.print(
|
|
789
1507
|
"[dim]Create one at: https://github.com/settings/tokens/new[/dim]"
|
|
@@ -799,17 +1517,17 @@ def init(
|
|
|
799
1517
|
# Validate required fields
|
|
800
1518
|
if not owner:
|
|
801
1519
|
console.print("[red]Error:[/red] GitHub repository owner is required")
|
|
802
|
-
raise typer.Exit(1)
|
|
1520
|
+
raise typer.Exit(1) from None
|
|
803
1521
|
|
|
804
1522
|
if not repo:
|
|
805
1523
|
console.print("[red]Error:[/red] GitHub repository name is required")
|
|
806
|
-
raise typer.Exit(1)
|
|
1524
|
+
raise typer.Exit(1) from None
|
|
807
1525
|
|
|
808
1526
|
if not token:
|
|
809
1527
|
console.print(
|
|
810
1528
|
"[red]Error:[/red] GitHub Personal Access Token is required"
|
|
811
1529
|
)
|
|
812
|
-
raise typer.Exit(1)
|
|
1530
|
+
raise typer.Exit(1) from None
|
|
813
1531
|
|
|
814
1532
|
config["adapters"]["github"] = {
|
|
815
1533
|
"owner": owner,
|
|
@@ -818,42 +1536,46 @@ def init(
|
|
|
818
1536
|
"type": "github",
|
|
819
1537
|
}
|
|
820
1538
|
|
|
821
|
-
# 5. Save to
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
config_file_path = resolver.GLOBAL_CONFIG_PATH
|
|
826
|
-
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
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)
|
|
827
1543
|
|
|
828
|
-
|
|
829
|
-
|
|
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
|
+
)
|
|
830
1551
|
|
|
831
|
-
|
|
832
|
-
|
|
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]")
|
|
833
1563
|
else:
|
|
834
|
-
#
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
with open(gitignore_path, "a") as f:
|
|
850
|
-
f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
|
|
851
|
-
console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
|
|
852
|
-
else:
|
|
853
|
-
# Create .gitignore if it doesn't exist
|
|
854
|
-
with open(gitignore_path, "w") as f:
|
|
855
|
-
f.write("# MCP Ticketer\n.mcp-ticketer/\n")
|
|
856
|
-
console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
|
|
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
|
|
857
1579
|
|
|
858
1580
|
# Show next steps
|
|
859
1581
|
_show_next_steps(console, adapter_type, config_file_path)
|
|
@@ -874,16 +1596,13 @@ def _show_next_steps(
|
|
|
874
1596
|
console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
|
|
875
1597
|
|
|
876
1598
|
console.print("[bold]Next Steps:[/bold]")
|
|
877
|
-
console.print("1. [cyan]
|
|
878
|
-
console.print(" mcp-ticketer diagnose")
|
|
879
|
-
console.print("\n2. [cyan]Create a test ticket:[/cyan]")
|
|
1599
|
+
console.print("1. [cyan]Create a test ticket:[/cyan]")
|
|
880
1600
|
console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
|
|
881
1601
|
|
|
882
1602
|
if adapter_type != "aitrackdown":
|
|
883
1603
|
console.print(
|
|
884
|
-
f"\
|
|
1604
|
+
f"\n2. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
|
|
885
1605
|
)
|
|
886
|
-
|
|
887
1606
|
if adapter_type == "linear":
|
|
888
1607
|
console.print(" Check your Linear workspace for the new ticket")
|
|
889
1608
|
elif adapter_type == "github":
|
|
@@ -891,16 +1610,19 @@ def _show_next_steps(
|
|
|
891
1610
|
elif adapter_type == "jira":
|
|
892
1611
|
console.print(" Check your JIRA project for the new ticket")
|
|
893
1612
|
else:
|
|
894
|
-
console.print("\
|
|
1613
|
+
console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
|
|
895
1614
|
console.print(" ls .aitrackdown/")
|
|
896
1615
|
|
|
897
|
-
console.print("\
|
|
1616
|
+
console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
|
|
898
1617
|
console.print(" mcp-ticketer install claude-code # For Claude Code")
|
|
899
1618
|
console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
|
|
900
1619
|
console.print(" mcp-ticketer install auggie # For Auggie")
|
|
901
1620
|
console.print(" mcp-ticketer install gemini # For Gemini CLI")
|
|
902
1621
|
|
|
903
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
|
+
)
|
|
904
1626
|
console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
|
|
905
1627
|
|
|
906
1628
|
|
|
@@ -1064,7 +1786,7 @@ def migrate_config(
|
|
|
1064
1786
|
|
|
1065
1787
|
|
|
1066
1788
|
@app.command("queue-status", deprecated=True, hidden=True)
|
|
1067
|
-
def old_queue_status_command():
|
|
1789
|
+
def old_queue_status_command() -> None:
|
|
1068
1790
|
"""Show queue and worker status.
|
|
1069
1791
|
|
|
1070
1792
|
DEPRECATED: Use 'mcp-ticketer queue status' instead.
|
|
@@ -1180,9 +1902,9 @@ def old_queue_health_command(
|
|
|
1180
1902
|
|
|
1181
1903
|
# Exit with appropriate code
|
|
1182
1904
|
if health["status"] == HealthStatus.CRITICAL:
|
|
1183
|
-
raise typer.Exit(1)
|
|
1905
|
+
raise typer.Exit(1) from None
|
|
1184
1906
|
elif health["status"] == HealthStatus.WARNING:
|
|
1185
|
-
raise typer.Exit(2)
|
|
1907
|
+
raise typer.Exit(2) from None
|
|
1186
1908
|
|
|
1187
1909
|
|
|
1188
1910
|
@app.command(deprecated=True, hidden=True)
|
|
@@ -1250,7 +1972,7 @@ def create(
|
|
|
1250
1972
|
console.print(
|
|
1251
1973
|
"[red]Cannot safely create ticket. Please check system status.[/red]"
|
|
1252
1974
|
)
|
|
1253
|
-
raise typer.Exit(1)
|
|
1975
|
+
raise typer.Exit(1) from None
|
|
1254
1976
|
else:
|
|
1255
1977
|
console.print(
|
|
1256
1978
|
"[green]✓ Auto-repair successful. Proceeding with ticket creation.[/green]"
|
|
@@ -1259,7 +1981,7 @@ def create(
|
|
|
1259
1981
|
console.print(
|
|
1260
1982
|
"[red]❌ No repair actions available. Manual intervention required.[/red]"
|
|
1261
1983
|
)
|
|
1262
|
-
raise typer.Exit(1)
|
|
1984
|
+
raise typer.Exit(1) from None
|
|
1263
1985
|
|
|
1264
1986
|
elif health["status"] == HealthStatus.WARNING:
|
|
1265
1987
|
console.print("[yellow]⚠️ Warning: Queue system has minor issues[/yellow]")
|
|
@@ -1438,7 +2160,7 @@ def list_tickets(
|
|
|
1438
2160
|
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket list' instead.[/yellow]\n"
|
|
1439
2161
|
)
|
|
1440
2162
|
|
|
1441
|
-
async def _list():
|
|
2163
|
+
async def _list() -> None:
|
|
1442
2164
|
adapter_instance = get_adapter(
|
|
1443
2165
|
override_adapter=adapter.value if adapter else None
|
|
1444
2166
|
)
|
|
@@ -1494,7 +2216,7 @@ def show(
|
|
|
1494
2216
|
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket show' instead.[/yellow]\n"
|
|
1495
2217
|
)
|
|
1496
2218
|
|
|
1497
|
-
async def _show():
|
|
2219
|
+
async def _show() -> None:
|
|
1498
2220
|
adapter_instance = get_adapter(
|
|
1499
2221
|
override_adapter=adapter.value if adapter else None
|
|
1500
2222
|
)
|
|
@@ -1508,7 +2230,7 @@ def show(
|
|
|
1508
2230
|
|
|
1509
2231
|
if not ticket:
|
|
1510
2232
|
console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
|
|
1511
|
-
raise typer.Exit(1)
|
|
2233
|
+
raise typer.Exit(1) from None
|
|
1512
2234
|
|
|
1513
2235
|
# Display ticket details
|
|
1514
2236
|
console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
|
|
@@ -1550,7 +2272,7 @@ def comment(
|
|
|
1550
2272
|
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket comment' instead.[/yellow]\n"
|
|
1551
2273
|
)
|
|
1552
2274
|
|
|
1553
|
-
async def _comment():
|
|
2275
|
+
async def _comment() -> None:
|
|
1554
2276
|
adapter_instance = get_adapter(
|
|
1555
2277
|
override_adapter=adapter.value if adapter else None
|
|
1556
2278
|
)
|
|
@@ -1573,7 +2295,7 @@ def comment(
|
|
|
1573
2295
|
console.print(f"Content: {content}")
|
|
1574
2296
|
except Exception as e:
|
|
1575
2297
|
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
1576
|
-
raise typer.Exit(1)
|
|
2298
|
+
raise typer.Exit(1) from e
|
|
1577
2299
|
|
|
1578
2300
|
|
|
1579
2301
|
@app.command(deprecated=True, hidden=True)
|
|
@@ -1612,7 +2334,7 @@ def update(
|
|
|
1612
2334
|
|
|
1613
2335
|
if not updates:
|
|
1614
2336
|
console.print("[yellow]No updates specified[/yellow]")
|
|
1615
|
-
raise typer.Exit(1)
|
|
2337
|
+
raise typer.Exit(1) from None
|
|
1616
2338
|
|
|
1617
2339
|
# Get the adapter name
|
|
1618
2340
|
config = load_config()
|
|
@@ -1684,7 +2406,7 @@ def transition(
|
|
|
1684
2406
|
" - Flag syntax (recommended): mcp-ticketer transition TICKET-ID --state STATE\n"
|
|
1685
2407
|
" - Positional syntax: mcp-ticketer transition TICKET-ID STATE"
|
|
1686
2408
|
)
|
|
1687
|
-
raise typer.Exit(1)
|
|
2409
|
+
raise typer.Exit(1) from None
|
|
1688
2410
|
|
|
1689
2411
|
# Get the adapter name
|
|
1690
2412
|
config = load_config()
|
|
@@ -1735,7 +2457,7 @@ def search(
|
|
|
1735
2457
|
"[yellow]⚠️ This command is deprecated. Use 'mcp-ticketer ticket search' instead.[/yellow]\n"
|
|
1736
2458
|
)
|
|
1737
2459
|
|
|
1738
|
-
async def _search():
|
|
2460
|
+
async def _search() -> None:
|
|
1739
2461
|
adapter_instance = get_adapter(
|
|
1740
2462
|
override_adapter=adapter.value if adapter else None
|
|
1741
2463
|
)
|
|
@@ -1777,10 +2499,13 @@ app.add_typer(queue_app, name="queue")
|
|
|
1777
2499
|
# Add discover command to main app
|
|
1778
2500
|
app.add_typer(discover_app, name="discover")
|
|
1779
2501
|
|
|
2502
|
+
# Add instructions command to main app
|
|
2503
|
+
app.add_typer(instruction_app, name="instructions")
|
|
2504
|
+
|
|
1780
2505
|
|
|
1781
2506
|
# Add diagnostics command
|
|
1782
|
-
@app.command("
|
|
1783
|
-
def
|
|
2507
|
+
@app.command("doctor")
|
|
2508
|
+
def doctor_command(
|
|
1784
2509
|
output_file: str | None = typer.Option(
|
|
1785
2510
|
None, "--output", "-o", help="Save full report to file"
|
|
1786
2511
|
),
|
|
@@ -1791,7 +2516,7 @@ def diagnose_command(
|
|
|
1791
2516
|
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1792
2517
|
),
|
|
1793
2518
|
) -> None:
|
|
1794
|
-
"""Run comprehensive system diagnostics and health check (alias:
|
|
2519
|
+
"""Run comprehensive system diagnostics and health check (alias: diagnose)."""
|
|
1795
2520
|
if simple:
|
|
1796
2521
|
from .simple_health import simple_diagnose
|
|
1797
2522
|
|
|
@@ -1807,7 +2532,7 @@ def diagnose_command(
|
|
|
1807
2532
|
|
|
1808
2533
|
console.print("\n" + json.dumps(report, indent=2))
|
|
1809
2534
|
if report["issues"]:
|
|
1810
|
-
raise typer.Exit(1)
|
|
2535
|
+
raise typer.Exit(1) from None
|
|
1811
2536
|
else:
|
|
1812
2537
|
try:
|
|
1813
2538
|
asyncio.run(
|
|
@@ -1823,11 +2548,11 @@ def diagnose_command(
|
|
|
1823
2548
|
|
|
1824
2549
|
report = simple_diagnose()
|
|
1825
2550
|
if report["issues"]:
|
|
1826
|
-
raise typer.Exit(1)
|
|
2551
|
+
raise typer.Exit(1) from None
|
|
1827
2552
|
|
|
1828
2553
|
|
|
1829
|
-
@app.command("
|
|
1830
|
-
def
|
|
2554
|
+
@app.command("diagnose", hidden=True)
|
|
2555
|
+
def diagnose_alias(
|
|
1831
2556
|
output_file: str | None = typer.Option(
|
|
1832
2557
|
None, "--output", "-o", help="Save full report to file"
|
|
1833
2558
|
),
|
|
@@ -1838,9 +2563,9 @@ def doctor_alias(
|
|
|
1838
2563
|
False, "--simple", help="Use simple diagnostics (no heavy dependencies)"
|
|
1839
2564
|
),
|
|
1840
2565
|
) -> None:
|
|
1841
|
-
"""Run comprehensive system diagnostics and health check (alias for
|
|
1842
|
-
# Call the
|
|
1843
|
-
|
|
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)
|
|
1844
2569
|
|
|
1845
2570
|
|
|
1846
2571
|
@app.command("status")
|
|
@@ -1850,7 +2575,7 @@ def status_command() -> None:
|
|
|
1850
2575
|
|
|
1851
2576
|
result = simple_health_check()
|
|
1852
2577
|
if result != 0:
|
|
1853
|
-
raise typer.Exit(result)
|
|
2578
|
+
raise typer.Exit(result) from None
|
|
1854
2579
|
|
|
1855
2580
|
|
|
1856
2581
|
@app.command("health")
|
|
@@ -1860,7 +2585,7 @@ def health_alias() -> None:
|
|
|
1860
2585
|
|
|
1861
2586
|
result = simple_health_check()
|
|
1862
2587
|
if result != 0:
|
|
1863
|
-
raise typer.Exit(result)
|
|
2588
|
+
raise typer.Exit(result) from None
|
|
1864
2589
|
|
|
1865
2590
|
|
|
1866
2591
|
# Create MCP configuration command group
|
|
@@ -1875,11 +2600,20 @@ mcp_app = typer.Typer(
|
|
|
1875
2600
|
@mcp_app.callback()
|
|
1876
2601
|
def mcp_callback(
|
|
1877
2602
|
ctx: typer.Context,
|
|
1878
|
-
project_path: str | None = typer.
|
|
1879
|
-
None, help="Project directory path (
|
|
2603
|
+
project_path: str | None = typer.Option(
|
|
2604
|
+
None, "--path", "-p", help="Project directory path (default: current directory)"
|
|
1880
2605
|
),
|
|
1881
|
-
):
|
|
1882
|
-
"""MCP command group - runs MCP server if no subcommand provided.
|
|
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
|
+
"""
|
|
1883
2617
|
if ctx.invoked_subcommand is None:
|
|
1884
2618
|
# No subcommand provided, run the serve command
|
|
1885
2619
|
# Change to project directory if provided
|
|
@@ -1897,6 +2631,17 @@ def install(
|
|
|
1897
2631
|
None,
|
|
1898
2632
|
help="Platform to install (claude-code, claude-desktop, gemini, codex, auggie)",
|
|
1899
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
|
+
),
|
|
1900
2645
|
adapter: str | None = typer.Option(
|
|
1901
2646
|
None,
|
|
1902
2647
|
"--adapter",
|
|
@@ -1950,26 +2695,247 @@ def install(
|
|
|
1950
2695
|
help="Show what would be done without making changes (for platform installation)",
|
|
1951
2696
|
),
|
|
1952
2697
|
) -> None:
|
|
1953
|
-
"""Install MCP for AI platforms
|
|
2698
|
+
"""Install MCP server configuration for AI platforms.
|
|
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.
|
|
1954
2706
|
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
|
1957
2713
|
|
|
1958
|
-
|
|
1959
|
-
|
|
2714
|
+
# Install for all detected platforms
|
|
2715
|
+
mcp-ticketer install --all
|
|
2716
|
+
|
|
2717
|
+
# Install for specific platform
|
|
1960
2718
|
mcp-ticketer install claude-code # Claude Code (project-level)
|
|
1961
2719
|
mcp-ticketer install claude-desktop # Claude Desktop (global)
|
|
1962
2720
|
mcp-ticketer install gemini # Gemini CLI
|
|
1963
2721
|
mcp-ticketer install codex # Codex
|
|
1964
2722
|
mcp-ticketer install auggie # Auggie
|
|
1965
2723
|
|
|
1966
|
-
Legacy
|
|
1967
|
-
mcp-ticketer install
|
|
1968
|
-
mcp-ticketer install --adapter linear
|
|
2724
|
+
Legacy Usage (adapter setup, deprecated - use 'init' or 'setup' instead):
|
|
2725
|
+
mcp-ticketer install --adapter linear # Use 'init' or 'setup' instead
|
|
1969
2726
|
|
|
1970
2727
|
"""
|
|
2728
|
+
from .platform_detection import PlatformDetector, get_platform_by_name
|
|
2729
|
+
|
|
2730
|
+
detector = PlatformDetector()
|
|
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
|
+
|
|
2884
|
+
console.print(
|
|
2885
|
+
"\n[dim]Enter the number of the platform to configure, or 'q' to quit:[/dim]"
|
|
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
|
+
|
|
1971
2903
|
# If platform argument is provided, handle MCP platform installation (NEW SYNTAX)
|
|
1972
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()
|
|
2908
|
+
)
|
|
2909
|
+
|
|
2910
|
+
if platform_info and not platform_info.is_installed:
|
|
2911
|
+
console.print(
|
|
2912
|
+
f"[yellow]⚠[/yellow] {platform_info.display_name} was detected but has a configuration issue."
|
|
2913
|
+
)
|
|
2914
|
+
console.print(f"[dim]Config path: {platform_info.config_path}[/dim]\n")
|
|
2915
|
+
|
|
2916
|
+
proceed = typer.confirm(
|
|
2917
|
+
"Do you want to proceed with installation anyway?", default=False
|
|
2918
|
+
)
|
|
2919
|
+
if not proceed:
|
|
2920
|
+
console.print("Installation cancelled.")
|
|
2921
|
+
return
|
|
2922
|
+
|
|
2923
|
+
elif not platform_info:
|
|
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
|
+
|
|
1973
2939
|
# Import configuration functions
|
|
1974
2940
|
from .auggie_configure import configure_auggie_mcp
|
|
1975
2941
|
from .codex_configure import configure_codex_mcp
|
|
@@ -2005,7 +2971,7 @@ def install(
|
|
|
2005
2971
|
console.print("\n[bold]Available platforms:[/bold]")
|
|
2006
2972
|
for p in platform_mapping.keys():
|
|
2007
2973
|
console.print(f" • {p}")
|
|
2008
|
-
raise typer.Exit(1)
|
|
2974
|
+
raise typer.Exit(1) from None
|
|
2009
2975
|
|
|
2010
2976
|
config = platform_mapping[platform]
|
|
2011
2977
|
|
|
@@ -2017,7 +2983,7 @@ def install(
|
|
|
2017
2983
|
config["func"]()
|
|
2018
2984
|
except Exception as e:
|
|
2019
2985
|
console.print(f"[red]Installation failed: {e}[/red]")
|
|
2020
|
-
raise typer.Exit(1)
|
|
2986
|
+
raise typer.Exit(1) from e
|
|
2021
2987
|
return
|
|
2022
2988
|
|
|
2023
2989
|
# Otherwise, delegate to init for adapter initialization (LEGACY BEHAVIOR)
|
|
@@ -2114,7 +3080,7 @@ def remove(
|
|
|
2114
3080
|
console.print("\n[bold]Available platforms:[/bold]")
|
|
2115
3081
|
for p in platform_mapping.keys():
|
|
2116
3082
|
console.print(f" • {p}")
|
|
2117
|
-
raise typer.Exit(1)
|
|
3083
|
+
raise typer.Exit(1) from None
|
|
2118
3084
|
|
|
2119
3085
|
config = platform_mapping[platform]
|
|
2120
3086
|
|
|
@@ -2122,7 +3088,7 @@ def remove(
|
|
|
2122
3088
|
config["func"]()
|
|
2123
3089
|
except Exception as e:
|
|
2124
3090
|
console.print(f"[red]Removal failed: {e}[/red]")
|
|
2125
|
-
raise typer.Exit(1)
|
|
3091
|
+
raise typer.Exit(1) from e
|
|
2126
3092
|
|
|
2127
3093
|
|
|
2128
3094
|
@app.command()
|
|
@@ -2161,7 +3127,7 @@ def uninstall(
|
|
|
2161
3127
|
|
|
2162
3128
|
|
|
2163
3129
|
@app.command(deprecated=True, hidden=True)
|
|
2164
|
-
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
3130
|
+
def check(queue_id: str = typer.Argument(..., help="Queue ID to check")) -> None:
|
|
2165
3131
|
"""Check status of a queued operation.
|
|
2166
3132
|
|
|
2167
3133
|
DEPRECATED: Use 'mcp-ticketer ticket check' instead.
|
|
@@ -2174,7 +3140,7 @@ def check(queue_id: str = typer.Argument(..., help="Queue ID to check")):
|
|
|
2174
3140
|
|
|
2175
3141
|
if not item:
|
|
2176
3142
|
console.print(f"[red]Queue item not found: {queue_id}[/red]")
|
|
2177
|
-
raise typer.Exit(1)
|
|
3143
|
+
raise typer.Exit(1) from None
|
|
2178
3144
|
|
|
2179
3145
|
# Display status
|
|
2180
3146
|
console.print(f"\n[bold]Queue Item: {item.id}[/bold]")
|
|
@@ -2216,7 +3182,7 @@ def mcp_serve(
|
|
|
2216
3182
|
base_path: str | None = typer.Option(
|
|
2217
3183
|
None, "--base-path", help="Base path for AITrackdown adapter"
|
|
2218
3184
|
),
|
|
2219
|
-
):
|
|
3185
|
+
) -> None:
|
|
2220
3186
|
"""Start MCP server for JSON-RPC communication over stdio.
|
|
2221
3187
|
|
|
2222
3188
|
This command is used by Claude Code/Desktop when connecting to the MCP server.
|
|
@@ -2236,7 +3202,7 @@ def mcp_serve(
|
|
|
2236
3202
|
# Load configuration (respects project-specific config in cwd)
|
|
2237
3203
|
config = load_config()
|
|
2238
3204
|
|
|
2239
|
-
# Determine adapter type with priority: CLI arg > .env files >
|
|
3205
|
+
# Determine adapter type with priority: CLI arg > config > .env files > default
|
|
2240
3206
|
if adapter:
|
|
2241
3207
|
# Priority 1: Command line argument
|
|
2242
3208
|
adapter_type = adapter.value
|
|
@@ -2244,18 +3210,24 @@ def mcp_serve(
|
|
|
2244
3210
|
adapters_config = config.get("adapters", {})
|
|
2245
3211
|
adapter_config = adapters_config.get(adapter_type, {})
|
|
2246
3212
|
else:
|
|
2247
|
-
# Priority 2:
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
env_config = _load_env_configuration()
|
|
2251
|
-
if env_config:
|
|
2252
|
-
adapter_type = env_config["adapter_type"]
|
|
2253
|
-
adapter_config = env_config["adapter_config"]
|
|
2254
|
-
else:
|
|
2255
|
-
# Priority 3: Configuration file
|
|
2256
|
-
adapter_type = config.get("default_adapter", "aitrackdown")
|
|
3213
|
+
# Priority 2: Configuration file (project-specific)
|
|
3214
|
+
adapter_type = config.get("default_adapter")
|
|
3215
|
+
if adapter_type:
|
|
2257
3216
|
adapters_config = config.get("adapters", {})
|
|
2258
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, {})
|
|
2259
3231
|
|
|
2260
3232
|
# Override with command line options if provided (highest priority)
|
|
2261
3233
|
if base_path and adapter_type == "aitrackdown":
|
|
@@ -2305,7 +3277,7 @@ def mcp_claude(
|
|
|
2305
3277
|
force: bool = typer.Option(
|
|
2306
3278
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2307
3279
|
),
|
|
2308
|
-
):
|
|
3280
|
+
) -> None:
|
|
2309
3281
|
"""Configure Claude Code to use mcp-ticketer MCP server.
|
|
2310
3282
|
|
|
2311
3283
|
Reads configuration from .mcp-ticketer/config.json and updates
|
|
@@ -2331,7 +3303,7 @@ def mcp_claude(
|
|
|
2331
3303
|
configure_claude_mcp(global_config=global_config, force=force)
|
|
2332
3304
|
except Exception as e:
|
|
2333
3305
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2334
|
-
raise typer.Exit(1)
|
|
3306
|
+
raise typer.Exit(1) from e
|
|
2335
3307
|
|
|
2336
3308
|
|
|
2337
3309
|
@mcp_app.command(name="gemini")
|
|
@@ -2345,7 +3317,7 @@ def mcp_gemini(
|
|
|
2345
3317
|
force: bool = typer.Option(
|
|
2346
3318
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2347
3319
|
),
|
|
2348
|
-
):
|
|
3320
|
+
) -> None:
|
|
2349
3321
|
"""Configure Gemini CLI to use mcp-ticketer MCP server.
|
|
2350
3322
|
|
|
2351
3323
|
Reads configuration from .mcp-ticketer/config.json and creates
|
|
@@ -2372,13 +3344,13 @@ def mcp_gemini(
|
|
|
2372
3344
|
console.print(
|
|
2373
3345
|
f"[red]✗ Invalid scope:[/red] '{scope}'. Must be 'project' or 'user'"
|
|
2374
3346
|
)
|
|
2375
|
-
raise typer.Exit(1)
|
|
3347
|
+
raise typer.Exit(1) from None
|
|
2376
3348
|
|
|
2377
3349
|
try:
|
|
2378
3350
|
configure_gemini_mcp(scope=scope, force=force) # type: ignore
|
|
2379
3351
|
except Exception as e:
|
|
2380
3352
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2381
|
-
raise typer.Exit(1)
|
|
3353
|
+
raise typer.Exit(1) from e
|
|
2382
3354
|
|
|
2383
3355
|
|
|
2384
3356
|
@mcp_app.command(name="codex")
|
|
@@ -2386,7 +3358,7 @@ def mcp_codex(
|
|
|
2386
3358
|
force: bool = typer.Option(
|
|
2387
3359
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2388
3360
|
),
|
|
2389
|
-
):
|
|
3361
|
+
) -> None:
|
|
2390
3362
|
"""Configure Codex CLI to use mcp-ticketer MCP server.
|
|
2391
3363
|
|
|
2392
3364
|
Reads configuration from .mcp-ticketer/config.json and creates
|
|
@@ -2410,7 +3382,7 @@ def mcp_codex(
|
|
|
2410
3382
|
configure_codex_mcp(force=force)
|
|
2411
3383
|
except Exception as e:
|
|
2412
3384
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2413
|
-
raise typer.Exit(1)
|
|
3385
|
+
raise typer.Exit(1) from e
|
|
2414
3386
|
|
|
2415
3387
|
|
|
2416
3388
|
@mcp_app.command(name="auggie")
|
|
@@ -2418,7 +3390,7 @@ def mcp_auggie(
|
|
|
2418
3390
|
force: bool = typer.Option(
|
|
2419
3391
|
False, "--force", "-f", help="Overwrite existing configuration"
|
|
2420
3392
|
),
|
|
2421
|
-
):
|
|
3393
|
+
) -> None:
|
|
2422
3394
|
"""Configure Auggie CLI to use mcp-ticketer MCP server.
|
|
2423
3395
|
|
|
2424
3396
|
Reads configuration from .mcp-ticketer/config.json and creates
|
|
@@ -2442,11 +3414,11 @@ def mcp_auggie(
|
|
|
2442
3414
|
configure_auggie_mcp(force=force)
|
|
2443
3415
|
except Exception as e:
|
|
2444
3416
|
console.print(f"[red]✗ Configuration failed:[/red] {e}")
|
|
2445
|
-
raise typer.Exit(1)
|
|
3417
|
+
raise typer.Exit(1) from e
|
|
2446
3418
|
|
|
2447
3419
|
|
|
2448
3420
|
@mcp_app.command(name="status")
|
|
2449
|
-
def mcp_status():
|
|
3421
|
+
def mcp_status() -> None:
|
|
2450
3422
|
"""Check MCP server status.
|
|
2451
3423
|
|
|
2452
3424
|
Shows whether the MCP server is configured and running for various platforms.
|
|
@@ -2544,7 +3516,7 @@ def mcp_status():
|
|
|
2544
3516
|
|
|
2545
3517
|
|
|
2546
3518
|
@mcp_app.command(name="stop")
|
|
2547
|
-
def mcp_stop():
|
|
3519
|
+
def mcp_stop() -> None:
|
|
2548
3520
|
"""Stop MCP server (placeholder - MCP runs on-demand via stdio).
|
|
2549
3521
|
|
|
2550
3522
|
Note: The MCP server runs on-demand when AI clients connect via stdio.
|
|
@@ -2568,8 +3540,8 @@ def mcp_stop():
|
|
|
2568
3540
|
app.add_typer(mcp_app, name="mcp")
|
|
2569
3541
|
|
|
2570
3542
|
|
|
2571
|
-
def main():
|
|
2572
|
-
"""
|
|
3543
|
+
def main() -> None:
|
|
3544
|
+
"""Execute the main CLI application entry point."""
|
|
2573
3545
|
app()
|
|
2574
3546
|
|
|
2575
3547
|
|