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.

Files changed (87) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +385 -6
  4. mcp_ticketer/adapters/asana/adapter.py +108 -0
  5. mcp_ticketer/adapters/asana/mappers.py +14 -0
  6. mcp_ticketer/adapters/github.py +525 -11
  7. mcp_ticketer/adapters/hybrid.py +47 -5
  8. mcp_ticketer/adapters/jira.py +521 -0
  9. mcp_ticketer/adapters/linear/adapter.py +1784 -101
  10. mcp_ticketer/adapters/linear/client.py +85 -3
  11. mcp_ticketer/adapters/linear/mappers.py +96 -8
  12. mcp_ticketer/adapters/linear/queries.py +168 -1
  13. mcp_ticketer/adapters/linear/types.py +80 -4
  14. mcp_ticketer/analysis/__init__.py +56 -0
  15. mcp_ticketer/analysis/dependency_graph.py +255 -0
  16. mcp_ticketer/analysis/health_assessment.py +304 -0
  17. mcp_ticketer/analysis/orphaned.py +218 -0
  18. mcp_ticketer/analysis/project_status.py +594 -0
  19. mcp_ticketer/analysis/similarity.py +224 -0
  20. mcp_ticketer/analysis/staleness.py +266 -0
  21. mcp_ticketer/automation/__init__.py +11 -0
  22. mcp_ticketer/automation/project_updates.py +378 -0
  23. mcp_ticketer/cli/adapter_diagnostics.py +3 -1
  24. mcp_ticketer/cli/auggie_configure.py +17 -5
  25. mcp_ticketer/cli/codex_configure.py +97 -61
  26. mcp_ticketer/cli/configure.py +851 -103
  27. mcp_ticketer/cli/cursor_configure.py +314 -0
  28. mcp_ticketer/cli/diagnostics.py +13 -12
  29. mcp_ticketer/cli/discover.py +5 -0
  30. mcp_ticketer/cli/gemini_configure.py +17 -5
  31. mcp_ticketer/cli/init_command.py +880 -0
  32. mcp_ticketer/cli/instruction_commands.py +6 -0
  33. mcp_ticketer/cli/main.py +233 -3151
  34. mcp_ticketer/cli/mcp_configure.py +672 -98
  35. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  36. mcp_ticketer/cli/platform_detection.py +77 -12
  37. mcp_ticketer/cli/platform_installer.py +536 -0
  38. mcp_ticketer/cli/project_update_commands.py +350 -0
  39. mcp_ticketer/cli/setup_command.py +639 -0
  40. mcp_ticketer/cli/simple_health.py +12 -10
  41. mcp_ticketer/cli/ticket_commands.py +264 -24
  42. mcp_ticketer/core/__init__.py +28 -6
  43. mcp_ticketer/core/adapter.py +166 -1
  44. mcp_ticketer/core/config.py +21 -21
  45. mcp_ticketer/core/exceptions.py +7 -1
  46. mcp_ticketer/core/label_manager.py +732 -0
  47. mcp_ticketer/core/mappers.py +31 -19
  48. mcp_ticketer/core/models.py +135 -0
  49. mcp_ticketer/core/onepassword_secrets.py +1 -1
  50. mcp_ticketer/core/priority_matcher.py +463 -0
  51. mcp_ticketer/core/project_config.py +132 -14
  52. mcp_ticketer/core/session_state.py +171 -0
  53. mcp_ticketer/core/state_matcher.py +592 -0
  54. mcp_ticketer/core/url_parser.py +425 -0
  55. mcp_ticketer/core/validators.py +69 -0
  56. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  57. mcp_ticketer/mcp/server/main.py +106 -25
  58. mcp_ticketer/mcp/server/routing.py +655 -0
  59. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  60. mcp_ticketer/mcp/server/tools/__init__.py +31 -12
  61. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  62. mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
  63. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  64. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  65. mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
  66. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  67. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
  68. mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
  69. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  70. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  71. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  72. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  73. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  74. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  75. mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
  76. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
  77. mcp_ticketer/queue/worker.py +1 -1
  78. mcp_ticketer/utils/__init__.py +5 -0
  79. mcp_ticketer/utils/token_utils.py +246 -0
  80. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  81. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  82. mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
  83. mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
  84. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  85. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  86. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  87. {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,880 @@
1
+ """Init command for mcp-ticketer - adapter configuration and initialization.
2
+
3
+ This module handles the initialization of adapter configuration through the
4
+ 'mcp-ticketer init' command. It provides:
5
+ - Auto-discovery of adapter configuration from .env files
6
+ - Interactive prompts for manual adapter configuration
7
+ - Configuration validation with retry loops
8
+ - Support for Linear, JIRA, GitHub, and AITrackdown adapters
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import os
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ import typer
18
+ from rich.console import Console
19
+
20
+ from .configure import (
21
+ _configure_aitrackdown,
22
+ _configure_github,
23
+ _configure_jira,
24
+ _configure_linear,
25
+ )
26
+
27
+ console = Console()
28
+
29
+
30
+ async def _validate_adapter_credentials(
31
+ adapter_type: str, config_file_path: Path
32
+ ) -> list[str]:
33
+ """Validate adapter credentials by performing real connectivity tests.
34
+
35
+ Args:
36
+ ----
37
+ adapter_type: Type of adapter to validate
38
+ config_file_path: Path to config file
39
+
40
+ Returns:
41
+ -------
42
+ List of validation issues (empty if valid)
43
+
44
+ """
45
+ issues = []
46
+
47
+ try:
48
+ # Load config
49
+ with open(config_file_path) as f:
50
+ config = json.load(f)
51
+
52
+ adapter_config = config.get("adapters", {}).get(adapter_type, {})
53
+
54
+ if not adapter_config:
55
+ issues.append(f"No configuration found for {adapter_type}")
56
+ return issues
57
+
58
+ # Validate based on adapter type
59
+ if adapter_type == "linear":
60
+ api_key = adapter_config.get("api_key")
61
+
62
+ # Check API key format
63
+ if not api_key:
64
+ issues.append("Linear API key is missing")
65
+ return issues
66
+
67
+ if not api_key.startswith("lin_api_"):
68
+ issues.append(
69
+ "Invalid Linear API key format (should start with 'lin_api_')"
70
+ )
71
+ return issues
72
+
73
+ # Test actual connectivity
74
+ try:
75
+ from ..adapters.linear import LinearAdapter
76
+
77
+ adapter = LinearAdapter(adapter_config)
78
+ # Try to list one ticket to verify connectivity
79
+ await adapter.list(limit=1)
80
+ except Exception as e:
81
+ error_msg = str(e)
82
+ if "401" in error_msg or "Unauthorized" in error_msg:
83
+ issues.append(
84
+ "Failed to authenticate with Linear API - invalid API key"
85
+ )
86
+ elif "403" in error_msg or "Forbidden" in error_msg:
87
+ issues.append("Linear API key lacks required permissions")
88
+ elif "team" in error_msg.lower():
89
+ issues.append(f"Linear team configuration error: {error_msg}")
90
+ else:
91
+ issues.append(f"Failed to connect to Linear API: {error_msg}")
92
+
93
+ elif adapter_type == "jira":
94
+ server = adapter_config.get("server")
95
+ email = adapter_config.get("email")
96
+ api_token = adapter_config.get("api_token")
97
+
98
+ # Check required fields
99
+ if not server:
100
+ issues.append("JIRA server URL is missing")
101
+ if not email:
102
+ issues.append("JIRA email is missing")
103
+ if not api_token:
104
+ issues.append("JIRA API token is missing")
105
+
106
+ if issues:
107
+ return issues
108
+
109
+ # Test actual connectivity
110
+ try:
111
+ from ..adapters.jira import JiraAdapter
112
+
113
+ adapter = JiraAdapter(adapter_config)
114
+ await adapter.list(limit=1)
115
+ except Exception as e:
116
+ error_msg = str(e)
117
+ if "401" in error_msg or "Unauthorized" in error_msg:
118
+ issues.append(
119
+ "Failed to authenticate with JIRA - invalid credentials"
120
+ )
121
+ elif "403" in error_msg or "Forbidden" in error_msg:
122
+ issues.append("JIRA credentials lack required permissions")
123
+ else:
124
+ issues.append(f"Failed to connect to JIRA: {error_msg}")
125
+
126
+ elif adapter_type == "github":
127
+ token = adapter_config.get("token") or adapter_config.get("api_key")
128
+ owner = adapter_config.get("owner")
129
+ repo = adapter_config.get("repo")
130
+
131
+ # Check required fields
132
+ if not token:
133
+ issues.append("GitHub token is missing")
134
+ if not owner:
135
+ issues.append("GitHub owner is missing")
136
+ if not repo:
137
+ issues.append("GitHub repo is missing")
138
+
139
+ if issues:
140
+ return issues
141
+
142
+ # Test actual connectivity
143
+ try:
144
+ from ..adapters.github import GitHubAdapter
145
+
146
+ adapter = GitHubAdapter(adapter_config)
147
+ await adapter.list(limit=1)
148
+ except Exception as e:
149
+ error_msg = str(e)
150
+ if (
151
+ "401" in error_msg
152
+ or "Unauthorized" in error_msg
153
+ or "Bad credentials" in error_msg
154
+ ):
155
+ issues.append("Failed to authenticate with GitHub - invalid token")
156
+ elif "404" in error_msg or "Not Found" in error_msg:
157
+ issues.append(f"GitHub repository not found: {owner}/{repo}")
158
+ elif "403" in error_msg or "Forbidden" in error_msg:
159
+ issues.append("GitHub token lacks required permissions")
160
+ else:
161
+ issues.append(f"Failed to connect to GitHub: {error_msg}")
162
+
163
+ elif adapter_type == "aitrackdown":
164
+ # AITrackdown doesn't require credentials, just check base_path is set
165
+ base_path = adapter_config.get("base_path")
166
+ if not base_path:
167
+ issues.append("AITrackdown base_path is missing")
168
+
169
+ except Exception as e:
170
+ issues.append(f"Validation error: {str(e)}")
171
+
172
+ return issues
173
+
174
+
175
+ async def _validate_configuration_with_retry(
176
+ console: Console, adapter_type: str, config_file_path: Path, proj_path: Path
177
+ ) -> bool:
178
+ """Validate configuration with retry loop for corrections.
179
+
180
+ Args:
181
+ ----
182
+ console: Rich console for output
183
+ adapter_type: Type of adapter configured
184
+ config_file_path: Path to config file
185
+ proj_path: Project path
186
+
187
+ Returns:
188
+ -------
189
+ True if validation passed or user chose to continue, False if user chose to exit
190
+
191
+ """
192
+ max_retries = 3
193
+ retry_count = 0
194
+
195
+ while retry_count < max_retries:
196
+ console.print("\n[cyan]🔍 Validating configuration...[/cyan]")
197
+
198
+ # Run real adapter validation (suppress verbose output)
199
+ import io
200
+ import sys
201
+
202
+ # Capture output to suppress verbose diagnostics output
203
+ old_stdout = sys.stdout
204
+ old_stderr = sys.stderr
205
+ sys.stdout = io.StringIO()
206
+ sys.stderr = io.StringIO()
207
+
208
+ try:
209
+ # Perform real adapter validation using diagnostics
210
+ validation_issues = await _validate_adapter_credentials(
211
+ adapter_type, config_file_path
212
+ )
213
+ finally:
214
+ # Restore stdout/stderr
215
+ sys.stdout = old_stdout
216
+ sys.stderr = old_stderr
217
+
218
+ # Check if there are issues
219
+ if not validation_issues:
220
+ console.print("[green]✓ Configuration validated successfully![/green]")
221
+ return True
222
+
223
+ # Display issues found
224
+ console.print("[yellow]⚠️ Configuration validation found issues:[/yellow]")
225
+ for issue in validation_issues:
226
+ console.print(f" [red]❌[/red] {issue}")
227
+
228
+ # Offer user options
229
+ console.print("\n[bold]What would you like to do?[/bold]")
230
+ console.print("1. [cyan]Re-enter configuration values[/cyan] (fix issues)")
231
+ console.print("2. [yellow]Continue anyway[/yellow] (skip validation)")
232
+ console.print("3. [red]Exit[/red] (fix manually later)")
233
+
234
+ try:
235
+ choice = typer.prompt("\nSelect option (1-3)", type=int, default=1)
236
+ except typer.Abort:
237
+ console.print("[yellow]Cancelled.[/yellow]")
238
+ return False
239
+
240
+ if choice == 1:
241
+ # Re-enter configuration
242
+ # Check BEFORE increment to fix off-by-one error
243
+ if retry_count >= max_retries:
244
+ console.print(
245
+ f"[red]Maximum retry attempts ({max_retries}) reached.[/red]"
246
+ )
247
+ console.print(
248
+ "[yellow]Please fix configuration manually and run 'mcp-ticketer doctor'[/yellow]"
249
+ )
250
+ return False
251
+ retry_count += 1
252
+
253
+ console.print(
254
+ f"\n[cyan]Retry {retry_count}/{max_retries} - Re-entering configuration...[/cyan]"
255
+ )
256
+
257
+ # Reload current config to get values
258
+ with open(config_file_path) as f:
259
+ current_config = json.load(f)
260
+
261
+ # Re-prompt for adapter-specific configuration using consolidated functions
262
+ try:
263
+ if adapter_type == "linear":
264
+ adapter_config, default_values = _configure_linear(interactive=True)
265
+ current_config["adapters"]["linear"] = adapter_config.to_dict()
266
+ # Merge default values into top-level config
267
+ if default_values.get("default_user"):
268
+ current_config["default_user"] = default_values["default_user"]
269
+ if default_values.get("default_epic"):
270
+ current_config["default_epic"] = default_values["default_epic"]
271
+ if default_values.get("default_project"):
272
+ current_config["default_project"] = default_values[
273
+ "default_project"
274
+ ]
275
+ if default_values.get("default_tags"):
276
+ current_config["default_tags"] = default_values["default_tags"]
277
+
278
+ elif adapter_type == "jira":
279
+ # Returns tuple: (AdapterConfig, default_values_dict)
280
+ adapter_config, default_values = _configure_jira(interactive=True)
281
+ current_config["adapters"]["jira"] = adapter_config.to_dict()
282
+
283
+ # Merge default values into top-level config
284
+ if default_values.get("default_user"):
285
+ current_config["default_user"] = default_values["default_user"]
286
+ if default_values.get("default_epic"):
287
+ current_config["default_epic"] = default_values["default_epic"]
288
+ if default_values.get("default_project"):
289
+ current_config["default_project"] = default_values[
290
+ "default_project"
291
+ ]
292
+ if default_values.get("default_tags"):
293
+ current_config["default_tags"] = default_values["default_tags"]
294
+
295
+ elif adapter_type == "github":
296
+ # Returns tuple: (AdapterConfig, default_values_dict)
297
+ adapter_config, default_values = _configure_github(interactive=True)
298
+ current_config["adapters"]["github"] = adapter_config.to_dict()
299
+
300
+ # Merge default values into top-level config
301
+ if default_values.get("default_user"):
302
+ current_config["default_user"] = default_values["default_user"]
303
+ if default_values.get("default_epic"):
304
+ current_config["default_epic"] = default_values["default_epic"]
305
+ if default_values.get("default_project"):
306
+ current_config["default_project"] = default_values[
307
+ "default_project"
308
+ ]
309
+ if default_values.get("default_tags"):
310
+ current_config["default_tags"] = default_values["default_tags"]
311
+
312
+ elif adapter_type == "aitrackdown":
313
+ # Returns tuple: (AdapterConfig, default_values_dict)
314
+ adapter_config, default_values = _configure_aitrackdown(
315
+ interactive=True
316
+ )
317
+ current_config["adapters"]["aitrackdown"] = adapter_config.to_dict()
318
+ # Save updated configuration
319
+ with open(config_file_path, "w") as f:
320
+ json.dump(current_config, f, indent=2)
321
+
322
+ console.print(
323
+ "[yellow]AITrackdown doesn't require credentials. Continuing...[/yellow]"
324
+ )
325
+ console.print("[dim]✓ Configuration updated[/dim]")
326
+ return True
327
+
328
+ else:
329
+ console.print(f"[red]Unknown adapter type: {adapter_type}[/red]")
330
+ return False
331
+
332
+ except (ValueError, typer.Exit) as e:
333
+ console.print(f"[red]Configuration error: {e}[/red]")
334
+ # Continue to retry loop
335
+ continue
336
+
337
+ # Save updated configuration
338
+ with open(config_file_path, "w") as f:
339
+ json.dump(current_config, f, indent=2)
340
+
341
+ console.print("[dim]✓ Configuration updated[/dim]")
342
+ # Loop will retry validation
343
+
344
+ elif choice == 2:
345
+ # Continue anyway
346
+ console.print(
347
+ "[yellow]⚠️ Continuing with potentially invalid configuration.[/yellow]"
348
+ )
349
+ console.print("[dim]You can validate later with: mcp-ticketer doctor[/dim]")
350
+ return True
351
+
352
+ elif choice == 3:
353
+ # Exit
354
+ console.print(
355
+ "[yellow]Configuration saved but not validated. Run 'mcp-ticketer doctor' to test.[/yellow]"
356
+ )
357
+ return False
358
+
359
+ else:
360
+ console.print(
361
+ f"[red]Invalid choice: {choice}. Please enter 1, 2, or 3.[/red]"
362
+ )
363
+ # Continue loop to ask again
364
+
365
+ return True
366
+
367
+
368
+ def _show_next_steps(
369
+ console: Console, adapter_type: str, config_file_path: Path
370
+ ) -> None:
371
+ """Show helpful next steps after initialization.
372
+
373
+ Args:
374
+ ----
375
+ console: Rich console for output
376
+ adapter_type: Type of adapter that was configured
377
+ config_file_path: Path to the configuration file
378
+
379
+ """
380
+ console.print("\n[bold green]🎉 Setup Complete![/bold green]")
381
+ console.print(f"MCP Ticketer is now configured to use {adapter_type.title()}.\n")
382
+
383
+ console.print("[bold]Next Steps:[/bold]")
384
+ console.print("1. [cyan]Create a test ticket:[/cyan]")
385
+ console.print(" mcp-ticketer create 'Test ticket from MCP Ticketer'")
386
+
387
+ if adapter_type != "aitrackdown":
388
+ console.print(
389
+ f"\n2. [cyan]Verify the ticket appears in {adapter_type.title()}[/cyan]"
390
+ )
391
+ if adapter_type == "linear":
392
+ console.print(" Check your Linear workspace for the new ticket")
393
+ elif adapter_type == "github":
394
+ console.print(" Check your GitHub repository's Issues tab")
395
+ elif adapter_type == "jira":
396
+ console.print(" Check your JIRA project for the new ticket")
397
+ else:
398
+ console.print("\n2. [cyan]Check local ticket storage:[/cyan]")
399
+ console.print(" ls .aitrackdown/")
400
+
401
+ console.print("\n3. [cyan]Install MCP for AI clients (optional):[/cyan]")
402
+ console.print(" mcp-ticketer install claude-code # For Claude Code")
403
+ console.print(" mcp-ticketer install claude-desktop # For Claude Desktop")
404
+ console.print(" mcp-ticketer install auggie # For Auggie")
405
+ console.print(" mcp-ticketer install gemini # For Gemini CLI")
406
+
407
+ console.print(f"\n[dim]Configuration saved to: {config_file_path}[/dim]")
408
+ console.print(
409
+ "[dim]Run 'mcp-ticketer doctor' to re-validate configuration anytime[/dim]"
410
+ )
411
+ console.print("[dim]Run 'mcp-ticketer --help' for more commands[/dim]")
412
+
413
+
414
+ def _init_adapter_internal(
415
+ adapter: str | None = None,
416
+ project_path: str | None = None,
417
+ global_config: bool = False,
418
+ base_path: str | None = None,
419
+ api_key: str | None = None,
420
+ team_id: str | None = None,
421
+ jira_server: str | None = None,
422
+ jira_email: str | None = None,
423
+ jira_project: str | None = None,
424
+ github_url: str | None = None,
425
+ github_token: str | None = None,
426
+ **kwargs: Any,
427
+ ) -> bool:
428
+ """Internal function to initialize adapter configuration.
429
+
430
+ This is the core business logic for adapter initialization, separated from
431
+ the Typer CLI command to allow programmatic calls from setup_command.py.
432
+
433
+ Args:
434
+ ----
435
+ adapter: Adapter type to use (interactive prompt if not specified)
436
+ project_path: Project path (default: current directory)
437
+ global_config: Save to global config instead of project-specific
438
+ base_path: Base path for ticket storage (AITrackdown only)
439
+ api_key: API key for Linear or API token for JIRA
440
+ team_id: Linear team ID (required for Linear adapter)
441
+ jira_server: JIRA server URL
442
+ jira_email: JIRA user email for authentication
443
+ jira_project: Default JIRA project key
444
+ github_url: GitHub repository URL (e.g., https://github.com/owner/repo)
445
+ github_token: GitHub Personal Access Token
446
+ **kwargs: Additional parameters (includes deprecated github_owner, github_repo for backward compatibility)
447
+
448
+ Returns:
449
+ -------
450
+ True if initialization succeeded, False otherwise
451
+
452
+ """
453
+ from ..core.env_discovery import discover_config
454
+
455
+ # Determine project path
456
+ proj_path = Path(project_path) if project_path else Path.cwd()
457
+
458
+ # Check if already initialized (unless using --global)
459
+ # Note: This check is skipped when called programmatically
460
+ # Callers should handle overwrite confirmation themselves
461
+
462
+ # 1. Try auto-discovery if no adapter specified
463
+ discovered = None
464
+ adapter_type = adapter
465
+
466
+ if not adapter_type:
467
+ console.print(
468
+ "[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
469
+ )
470
+
471
+ # First try our improved .env configuration loader
472
+ from ..mcp.server.main import _load_env_configuration
473
+
474
+ env_config = _load_env_configuration()
475
+
476
+ if env_config:
477
+ adapter_type = env_config["adapter_type"]
478
+ console.print(
479
+ f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
480
+ )
481
+
482
+ # Show what was discovered
483
+ console.print("\n[dim]Configuration found in: .env files[/dim]")
484
+ console.print("[dim]Confidence: 100%[/dim]")
485
+
486
+ # Use auto-detected adapter in programmatic mode
487
+ # Interactive mode will be handled by the CLI wrapper
488
+ # For programmatic calls, we accept the detected adapter
489
+ else:
490
+ # Fallback to old discovery system for backward compatibility
491
+ discovered = discover_config(proj_path)
492
+
493
+ if discovered and discovered.adapters:
494
+ primary = discovered.get_primary_adapter()
495
+ if primary:
496
+ adapter_type = primary.adapter_type
497
+ console.print(
498
+ f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
499
+ )
500
+
501
+ # Show what was discovered
502
+ console.print(
503
+ f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
504
+ )
505
+ console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
506
+
507
+ # Use auto-detected adapter in programmatic mode
508
+ # Interactive confirmation will be handled by the CLI wrapper
509
+ else:
510
+ adapter_type = None # Will trigger interactive selection
511
+ else:
512
+ adapter_type = None # Will trigger interactive selection
513
+
514
+ # If no adapter determined, fail in programmatic mode
515
+ # (interactive selection will be handled by CLI wrapper)
516
+ if not adapter_type:
517
+ console.print(
518
+ "[red]Error: Could not determine adapter type. "
519
+ "Please specify --adapter or set environment variables.[/red]"
520
+ )
521
+ return False
522
+
523
+ # 2. Create configuration based on adapter type
524
+ config = {"default_adapter": adapter_type, "adapters": {}}
525
+
526
+ # 3. If discovered and matches adapter_type, use discovered config
527
+ if discovered and adapter_type != "aitrackdown":
528
+ discovered_adapter = discovered.get_adapter_by_type(adapter_type)
529
+ if discovered_adapter:
530
+ adapter_config = discovered_adapter.config.copy()
531
+ # Ensure the config has the correct 'type' field
532
+ adapter_config["type"] = adapter_type
533
+ # Remove 'adapter' field if present (legacy)
534
+ adapter_config.pop("adapter", None)
535
+ config["adapters"][adapter_type] = adapter_config
536
+
537
+ # 4. Handle manual configuration for specific adapters
538
+ if adapter_type == "aitrackdown":
539
+ config["adapters"]["aitrackdown"] = {
540
+ "type": "aitrackdown",
541
+ "base_path": base_path or ".aitrackdown",
542
+ }
543
+
544
+ elif adapter_type == "linear":
545
+ # If not auto-discovered, build from CLI params or use consolidated function
546
+ if adapter_type not in config["adapters"]:
547
+ try:
548
+ # Determine if we need interactive prompts
549
+ has_all_params = bool(
550
+ (api_key or os.getenv("LINEAR_API_KEY"))
551
+ and (
552
+ team_id
553
+ or os.getenv("LINEAR_TEAM_ID")
554
+ or os.getenv("LINEAR_TEAM_KEY")
555
+ )
556
+ )
557
+
558
+ # Use consolidated configure function (interactive if missing params)
559
+ adapter_config, default_values = _configure_linear(
560
+ interactive=not has_all_params,
561
+ api_key=api_key,
562
+ team_id=team_id,
563
+ )
564
+
565
+ config["adapters"]["linear"] = adapter_config.to_dict()
566
+
567
+ # Merge default values into top-level config
568
+ if default_values.get("default_user"):
569
+ config["default_user"] = default_values["default_user"]
570
+ if default_values.get("default_epic"):
571
+ config["default_epic"] = default_values["default_epic"]
572
+ if default_values.get("default_project"):
573
+ config["default_project"] = default_values["default_project"]
574
+ if default_values.get("default_tags"):
575
+ config["default_tags"] = default_values["default_tags"]
576
+
577
+ except ValueError as e:
578
+ console.print(f"[red]Error:[/red] {e}")
579
+ return False
580
+
581
+ elif adapter_type == "jira":
582
+ # If not auto-discovered, build from CLI params or use consolidated function
583
+ if adapter_type not in config["adapters"]:
584
+ try:
585
+ # Determine if we need interactive prompts
586
+ has_all_params = bool(
587
+ (jira_server or os.getenv("JIRA_SERVER"))
588
+ and (jira_email or os.getenv("JIRA_EMAIL"))
589
+ and (api_key or os.getenv("JIRA_API_TOKEN"))
590
+ )
591
+
592
+ # Use consolidated configure function (interactive if missing params)
593
+ # Returns tuple: (AdapterConfig, default_values_dict) - following Linear pattern
594
+ adapter_config, default_values = _configure_jira(
595
+ interactive=not has_all_params,
596
+ server=jira_server,
597
+ email=jira_email,
598
+ api_token=api_key,
599
+ project_key=jira_project,
600
+ )
601
+
602
+ config["adapters"]["jira"] = adapter_config.to_dict()
603
+
604
+ # Merge default values into top-level config
605
+ if default_values.get("default_user"):
606
+ config["default_user"] = default_values["default_user"]
607
+ if default_values.get("default_epic"):
608
+ config["default_epic"] = default_values["default_epic"]
609
+ if default_values.get("default_project"):
610
+ config["default_project"] = default_values["default_project"]
611
+ if default_values.get("default_tags"):
612
+ config["default_tags"] = default_values["default_tags"]
613
+
614
+ except ValueError as e:
615
+ console.print(f"[red]Error:[/red] {e}")
616
+ return False
617
+
618
+ elif adapter_type == "github":
619
+ # If not auto-discovered, build from CLI params or use consolidated function
620
+ if adapter_type not in config["adapters"]:
621
+ try:
622
+ # Extract deprecated parameters for backward compatibility
623
+ github_owner = kwargs.get("github_owner")
624
+ github_repo = kwargs.get("github_repo")
625
+
626
+ # Determine if we need interactive prompts
627
+ # Prioritize github_url, fallback to owner/repo
628
+ has_all_params = bool(
629
+ (
630
+ github_url
631
+ or os.getenv("GITHUB_REPO_URL")
632
+ or (github_owner or os.getenv("GITHUB_OWNER"))
633
+ and (github_repo or os.getenv("GITHUB_REPO"))
634
+ )
635
+ and (github_token or os.getenv("GITHUB_TOKEN"))
636
+ )
637
+
638
+ # Use consolidated configure function (interactive if missing params)
639
+ # Returns tuple: (AdapterConfig, default_values_dict) - following Linear pattern
640
+ adapter_config, default_values = _configure_github(
641
+ interactive=not has_all_params,
642
+ repo_url=github_url,
643
+ owner=github_owner,
644
+ repo=github_repo,
645
+ token=github_token,
646
+ )
647
+
648
+ config["adapters"]["github"] = adapter_config.to_dict()
649
+
650
+ # Merge default values into top-level config
651
+ if default_values.get("default_user"):
652
+ config["default_user"] = default_values["default_user"]
653
+ if default_values.get("default_epic"):
654
+ config["default_epic"] = default_values["default_epic"]
655
+ if default_values.get("default_project"):
656
+ config["default_project"] = default_values["default_project"]
657
+ if default_values.get("default_tags"):
658
+ config["default_tags"] = default_values["default_tags"]
659
+
660
+ except ValueError as e:
661
+ console.print(f"[red]Error:[/red] {e}")
662
+ return False
663
+
664
+ # 5. Save to project-local config (global config deprecated for security)
665
+ # Always save to ./.mcp-ticketer/config.json (PROJECT-SPECIFIC)
666
+ config_file_path = proj_path / ".mcp-ticketer" / "config.json"
667
+ config_file_path.parent.mkdir(parents=True, exist_ok=True)
668
+
669
+ with open(config_file_path, "w") as f:
670
+ json.dump(config, f, indent=2)
671
+
672
+ if global_config:
673
+ console.print(
674
+ "[yellow]Note: Global config deprecated for security. Saved to project config instead.[/yellow]"
675
+ )
676
+
677
+ console.print(f"[green]✓ Initialized with {adapter_type} adapter[/green]")
678
+ console.print(f"[dim]Project configuration saved to {config_file_path}[/dim]")
679
+
680
+ # Add .mcp-ticketer to .gitignore if not already there
681
+ gitignore_path = proj_path / ".gitignore"
682
+ if gitignore_path.exists():
683
+ gitignore_content = gitignore_path.read_text()
684
+ if ".mcp-ticketer" not in gitignore_content:
685
+ with open(gitignore_path, "a") as f:
686
+ f.write("\n# MCP Ticketer\n.mcp-ticketer/\n")
687
+ console.print("[dim]✓ Added .mcp-ticketer/ to .gitignore[/dim]")
688
+ else:
689
+ # Create .gitignore if it doesn't exist
690
+ with open(gitignore_path, "w") as f:
691
+ f.write("# MCP Ticketer\n.mcp-ticketer/\n")
692
+ console.print("[dim]✓ Created .gitignore with .mcp-ticketer/[/dim]")
693
+
694
+ # Validate configuration with loop for corrections
695
+ if not asyncio.run(
696
+ _validate_configuration_with_retry(
697
+ console, adapter_type, config_file_path, proj_path
698
+ )
699
+ ):
700
+ # User chose to exit without valid configuration
701
+ return False
702
+
703
+ # Show next steps
704
+ _show_next_steps(console, adapter_type, config_file_path)
705
+ return True
706
+
707
+
708
+ def init(
709
+ adapter: str | None = typer.Option(
710
+ None,
711
+ "--adapter",
712
+ "-a",
713
+ help="Adapter type to use (interactive prompt if not specified)",
714
+ ),
715
+ project_path: str | None = typer.Option(
716
+ None, "--path", help="Project path (default: current directory)"
717
+ ),
718
+ global_config: bool = typer.Option(
719
+ False,
720
+ "--global",
721
+ "-g",
722
+ help="Save to global config instead of project-specific",
723
+ ),
724
+ base_path: str | None = typer.Option(
725
+ None,
726
+ "--base-path",
727
+ "-p",
728
+ help="Base path for ticket storage (AITrackdown only)",
729
+ ),
730
+ api_key: str | None = typer.Option(
731
+ None, "--api-key", help="API key for Linear or API token for JIRA"
732
+ ),
733
+ team_id: str | None = typer.Option(
734
+ None, "--team-id", help="Linear team ID (required for Linear adapter)"
735
+ ),
736
+ jira_server: str | None = typer.Option(
737
+ None,
738
+ "--jira-server",
739
+ help="JIRA server URL (e.g., https://company.atlassian.net)",
740
+ ),
741
+ jira_email: str | None = typer.Option(
742
+ None, "--jira-email", help="JIRA user email for authentication"
743
+ ),
744
+ jira_project: str | None = typer.Option(
745
+ None, "--jira-project", help="Default JIRA project key"
746
+ ),
747
+ github_url: str | None = typer.Option(
748
+ None,
749
+ "--github-url",
750
+ help="GitHub repository URL (e.g., https://github.com/owner/repo)",
751
+ ),
752
+ github_token: str | None = typer.Option(
753
+ None, "--github-token", help="GitHub Personal Access Token"
754
+ ),
755
+ # Deprecated parameters for backward compatibility (hidden from help)
756
+ github_owner: str | None = typer.Option(None, "--github-owner", hidden=True),
757
+ github_repo: str | None = typer.Option(None, "--github-repo", hidden=True),
758
+ ) -> None:
759
+ """Initialize adapter configuration only (without platform installation).
760
+
761
+ This command sets up adapter configuration with interactive prompts.
762
+ It auto-detects adapter configuration from .env files or prompts for
763
+ interactive setup if no configuration is found.
764
+
765
+ Creates .mcp-ticketer/config.json in the current directory.
766
+
767
+ RECOMMENDED: Use 'mcp-ticketer setup' instead for a complete setup
768
+ experience that includes both adapter configuration and platform
769
+ installation in one command.
770
+
771
+ The init command automatically validates your configuration after setup:
772
+ - If validation passes, setup completes
773
+ - If issues are detected, you can re-enter credentials, continue anyway, or exit
774
+ - You get up to 3 retry attempts to fix configuration issues
775
+ - You can always re-validate later with 'mcp-ticketer doctor'
776
+
777
+ Examples:
778
+ --------
779
+ # For first-time setup, use 'setup' instead (recommended)
780
+ mcp-ticketer setup
781
+
782
+ # Initialize adapter only (advanced usage)
783
+ mcp-ticketer init
784
+
785
+ # Force specific adapter
786
+ mcp-ticketer init --adapter linear
787
+
788
+ # Initialize for different project
789
+ mcp-ticketer init --path /path/to/project
790
+
791
+ """
792
+ from .setup_command import _prompt_for_adapter_selection
793
+
794
+ # Determine project path
795
+ proj_path = Path(project_path) if project_path else Path.cwd()
796
+
797
+ # Check if already initialized (unless using --global)
798
+ if not global_config:
799
+ config_path = proj_path / ".mcp-ticketer" / "config.json"
800
+
801
+ if config_path.exists():
802
+ if not typer.confirm(
803
+ f"Configuration already exists at {config_path}. Overwrite?",
804
+ default=False,
805
+ ):
806
+ console.print("[yellow]Initialization cancelled.[/yellow]")
807
+ raise typer.Exit(0) from None
808
+
809
+ # Handle interactive adapter selection if needed
810
+ adapter_type = adapter
811
+ if not adapter_type:
812
+ # Try auto-discovery first
813
+ console.print(
814
+ "[cyan]🔍 Auto-discovering configuration from .env files...[/cyan]"
815
+ )
816
+
817
+ from ..core.env_discovery import discover_config
818
+ from ..mcp.server.main import _load_env_configuration
819
+
820
+ env_config = _load_env_configuration()
821
+
822
+ if env_config:
823
+ adapter_type = env_config["adapter_type"]
824
+ console.print(
825
+ f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
826
+ )
827
+ console.print("\n[dim]Configuration found in: .env files[/dim]")
828
+ console.print("[dim]Confidence: 100%[/dim]")
829
+
830
+ # Ask user to confirm auto-detected adapter
831
+ if not typer.confirm(
832
+ f"Use detected {adapter_type} adapter?",
833
+ default=True,
834
+ ):
835
+ adapter_type = None
836
+ else:
837
+ # Fallback to old discovery
838
+ discovered = discover_config(proj_path)
839
+ if discovered and discovered.adapters:
840
+ primary = discovered.get_primary_adapter()
841
+ if primary:
842
+ adapter_type = primary.adapter_type
843
+ console.print(
844
+ f"[green]✓ Detected {adapter_type} adapter from environment files[/green]"
845
+ )
846
+ console.print(
847
+ f"\n[dim]Configuration found in: {primary.found_in}[/dim]"
848
+ )
849
+ console.print(f"[dim]Confidence: {primary.confidence:.0%}[/dim]")
850
+
851
+ if not typer.confirm(
852
+ f"Use detected {adapter_type} adapter?",
853
+ default=True,
854
+ ):
855
+ adapter_type = None
856
+
857
+ # If still no adapter, show interactive selection
858
+ if not adapter_type:
859
+ adapter_type = _prompt_for_adapter_selection(console)
860
+
861
+ # Call internal function with extracted values
862
+ success = _init_adapter_internal(
863
+ adapter=adapter_type,
864
+ project_path=project_path,
865
+ global_config=global_config,
866
+ base_path=base_path,
867
+ api_key=api_key,
868
+ team_id=team_id,
869
+ jira_server=jira_server,
870
+ jira_email=jira_email,
871
+ jira_project=jira_project,
872
+ github_url=github_url,
873
+ github_token=github_token,
874
+ # Pass deprecated parameters via kwargs for backward compatibility
875
+ github_owner=github_owner,
876
+ github_repo=github_repo,
877
+ )
878
+
879
+ if not success:
880
+ raise typer.Exit(1) from None