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
@@ -1,6 +1,7 @@
1
1
  """Interactive configuration wizard for MCP Ticketer."""
2
2
 
3
3
  import os
4
+ from collections.abc import Callable
4
5
  from typing import Any
5
6
 
6
7
  import typer
@@ -22,6 +23,61 @@ from ..core.project_config import (
22
23
  console = Console()
23
24
 
24
25
 
26
+ def _retry_setting(
27
+ setting_name: str,
28
+ prompt_func: Callable[[], Any],
29
+ validate_func: Callable[[Any], tuple[bool, str | None]],
30
+ max_retries: int = 3,
31
+ ) -> Any:
32
+ """Retry a configuration setting with validation.
33
+
34
+ Args:
35
+ ----
36
+ setting_name: Human-readable name of the setting
37
+ prompt_func: Function that prompts for the setting value
38
+ validate_func: Function that validates the value (returns tuple of success, error_msg)
39
+ max_retries: Maximum number of retry attempts
40
+
41
+ Returns:
42
+ -------
43
+ Validated setting value
44
+
45
+ Raises:
46
+ ------
47
+ typer.Exit: If max retries exceeded
48
+
49
+ """
50
+ for attempt in range(1, max_retries + 1):
51
+ try:
52
+ value = prompt_func()
53
+ is_valid, error = validate_func(value)
54
+
55
+ if is_valid:
56
+ return value
57
+ console.print(f"[red]✗ {error}[/red]")
58
+ if attempt < max_retries:
59
+ console.print(
60
+ f"[yellow]Attempt {attempt}/{max_retries} - Please try again[/yellow]"
61
+ )
62
+ else:
63
+ console.print(f"[red]Failed after {max_retries} attempts[/red]")
64
+ retry = Confirm.ask("Retry this setting?", default=True)
65
+ if retry:
66
+ # Extend attempts
67
+ max_retries += 3
68
+ console.print(
69
+ f"[yellow]Extending retries (new limit: {max_retries})[/yellow]"
70
+ )
71
+ continue
72
+ raise typer.Exit(1) from None
73
+ except KeyboardInterrupt:
74
+ console.print("\n[yellow]Configuration cancelled[/yellow]")
75
+ raise typer.Exit(0) from None
76
+
77
+ console.print(f"[red]Could not configure {setting_name}[/red]")
78
+ raise typer.Exit(1)
79
+
80
+
25
81
  def configure_wizard() -> None:
26
82
  """Run interactive configuration wizard."""
27
83
  console.print(
@@ -97,173 +153,853 @@ def _configure_single_adapter() -> TicketerConfig:
97
153
  adapter_type = adapter_type_map[adapter_choice]
98
154
 
99
155
  # Configure the selected adapter
156
+ default_values: dict[str, str] = {}
100
157
  if adapter_type == AdapterType.LINEAR:
101
- adapter_config = _configure_linear()
158
+ adapter_config, default_values = _configure_linear(interactive=True)
102
159
  elif adapter_type == AdapterType.JIRA:
103
- adapter_config = _configure_jira()
160
+ adapter_config, default_values = _configure_jira(interactive=True)
104
161
  elif adapter_type == AdapterType.GITHUB:
105
- adapter_config = _configure_github()
162
+ adapter_config, default_values = _configure_github(interactive=True)
106
163
  else:
107
- adapter_config = _configure_aitrackdown()
164
+ adapter_config, default_values = _configure_aitrackdown(interactive=True)
108
165
 
109
- # Create config
166
+ # Create config with default values
110
167
  config = TicketerConfig(
111
168
  default_adapter=adapter_type.value,
112
169
  adapters={adapter_type.value: adapter_config},
170
+ default_user=default_values.get("default_user"),
171
+ default_project=default_values.get("default_project"),
172
+ default_epic=default_values.get("default_epic"),
173
+ default_tags=default_values.get("default_tags"),
113
174
  )
114
175
 
115
176
  return config
116
177
 
117
178
 
118
- def _configure_linear() -> AdapterConfig:
119
- """Configure Linear adapter."""
120
- console.print("\n[bold]Configure Linear Integration:[/bold]")
179
+ def _configure_linear(
180
+ existing_config: dict[str, Any] | None = None,
181
+ interactive: bool = True,
182
+ api_key: str | None = None,
183
+ team_id: str | None = None,
184
+ team_key: str | None = None,
185
+ **kwargs: Any,
186
+ ) -> tuple[AdapterConfig, dict[str, Any]]:
187
+ """Configure Linear adapter with option to preserve existing settings.
121
188
 
122
- # API Key
123
- api_key = os.getenv("LINEAR_API_KEY") or ""
124
- if api_key:
125
- console.print("[dim]Found LINEAR_API_KEY in environment[/dim]")
126
- use_env = Confirm.ask("Use this API key?", default=True)
127
- if not use_env:
128
- api_key = ""
189
+ Supports both interactive (wizard) and programmatic (init command) modes.
129
190
 
130
- if not api_key:
131
- api_key = Prompt.ask("Linear API Key", password=True)
191
+ Args:
192
+ ----
193
+ existing_config: Optional existing configuration to preserve/update
194
+ interactive: If True, prompt user for missing values (default: True)
195
+ api_key: Pre-provided API key (optional, for programmatic mode)
196
+ team_id: Pre-provided team ID (optional, for programmatic mode)
197
+ team_key: Pre-provided team key (optional, for programmatic mode)
198
+ **kwargs: Additional configuration parameters
199
+
200
+ Returns:
201
+ -------
202
+ Tuple of (AdapterConfig, default_values_dict)
203
+ - AdapterConfig: Configured Linear adapter configuration
204
+ - default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
132
205
 
133
- # Team ID
134
- team_id = Prompt.ask("Team ID (optional, e.g., team-abc)", default="")
206
+ """
207
+ if interactive:
208
+ console.print("\n[bold cyan]Linear Configuration[/bold cyan]")
135
209
 
136
- # Team Key
137
- team_key = Prompt.ask("Team Key (optional, e.g., ENG)", default="")
210
+ # Check if we have existing config
211
+ has_existing = (
212
+ existing_config is not None and existing_config.get("adapter") == "linear"
213
+ )
138
214
 
139
- # Project ID
140
- project_id = Prompt.ask("Project ID (optional)", default="")
215
+ config_dict: dict[str, Any] = {"adapter": AdapterType.LINEAR.value}
141
216
 
142
- config_dict = {
143
- "adapter": AdapterType.LINEAR.value,
144
- "api_key": api_key,
145
- }
217
+ if has_existing and interactive:
218
+ preserve = Confirm.ask(
219
+ "Existing Linear configuration found. Preserve current settings?",
220
+ default=True,
221
+ )
222
+ if preserve:
223
+ console.print("[green]✓[/green] Keeping existing configuration")
224
+ config_dict = existing_config.copy()
225
+
226
+ # Allow updating specific fields
227
+ update_fields = Confirm.ask("Update specific fields?", default=False)
228
+ if not update_fields:
229
+ # Extract default values before returning
230
+ default_values = {}
231
+ if "user_email" in config_dict:
232
+ default_values["default_user"] = config_dict.pop("user_email")
233
+ if "default_epic" in config_dict:
234
+ default_values["default_epic"] = config_dict.pop("default_epic")
235
+ if "default_project" in config_dict:
236
+ default_values["default_project"] = config_dict.pop(
237
+ "default_project"
238
+ )
239
+ if "default_tags" in config_dict:
240
+ default_values["default_tags"] = config_dict.pop("default_tags")
241
+ return AdapterConfig.from_dict(config_dict), default_values
242
+
243
+ console.print(
244
+ "[yellow]Enter new values or press Enter to keep current[/yellow]"
245
+ )
146
246
 
147
- if team_id:
148
- config_dict["team_id"] = team_id
149
- if team_key:
150
- config_dict["team_key"] = team_key
151
- if project_id:
152
- config_dict["project_id"] = project_id
247
+ # API Key (programmatic mode: use provided value or env, interactive: prompt)
248
+ current_key = config_dict.get("api_key", "") if has_existing else ""
249
+ final_api_key = api_key or os.getenv("LINEAR_API_KEY") or ""
250
+
251
+ if interactive:
252
+ # Interactive mode: prompt with retry
253
+ if final_api_key and not current_key:
254
+ console.print("[dim]Found LINEAR_API_KEY in environment[/dim]")
255
+ use_env = Confirm.ask("Use this API key?", default=True)
256
+ if use_env:
257
+ current_key = final_api_key
258
+
259
+ def prompt_api_key() -> str:
260
+ if current_key:
261
+ api_key_prompt = f"Linear API Key [current: {'*' * 8}...]"
262
+ return Prompt.ask(api_key_prompt, password=True, default=current_key)
263
+ return Prompt.ask("Linear API Key", password=True)
264
+
265
+ def validate_api_key(key: str) -> tuple[bool, str | None]:
266
+ if not key or len(key) < 10:
267
+ return False, "API key must be at least 10 characters"
268
+ return True, None
269
+
270
+ final_api_key = _retry_setting("API Key", prompt_api_key, validate_api_key)
271
+ elif not final_api_key:
272
+ raise ValueError(
273
+ "Linear API key is required (provide api_key parameter or set LINEAR_API_KEY environment variable)"
274
+ )
153
275
 
154
- # Validate
276
+ config_dict["api_key"] = final_api_key
277
+
278
+ # Team Key/ID (programmatic mode: use provided values, interactive: prompt)
279
+ current_team_key = config_dict.get("team_key", "") if has_existing else ""
280
+ config_dict.get("team_id", "") if has_existing else ""
281
+ final_team_key = team_key or os.getenv("LINEAR_TEAM_KEY") or ""
282
+ final_team_id = team_id or os.getenv("LINEAR_TEAM_ID") or ""
283
+
284
+ if interactive:
285
+ # Interactive mode: prompt for team key (preferred over team_id)
286
+ def prompt_team_key() -> str:
287
+ if current_team_key:
288
+ team_key_prompt = f"Linear Team Key [current: {current_team_key}]"
289
+ return Prompt.ask(team_key_prompt, default=current_team_key)
290
+ return Prompt.ask("Linear Team Key (e.g., 'ENG', 'BTA')")
291
+
292
+ def validate_team_key(key: str) -> tuple[bool, str | None]:
293
+ if not key or len(key) < 2:
294
+ return False, "Team key must be at least 2 characters"
295
+ return True, None
296
+
297
+ final_team_key = _retry_setting("Team Key", prompt_team_key, validate_team_key)
298
+ config_dict["team_key"] = final_team_key
299
+
300
+ # Remove team_id if present (will be resolved from team_key)
301
+ if "team_id" in config_dict:
302
+ del config_dict["team_id"]
303
+ else:
304
+ # Programmatic mode: use whichever was provided
305
+ if final_team_key:
306
+ config_dict["team_key"] = final_team_key
307
+ if final_team_id:
308
+ config_dict["team_id"] = final_team_id
309
+ if not final_team_key and not final_team_id:
310
+ raise ValueError(
311
+ "Linear requires either team_key or team_id (provide parameter or set LINEAR_TEAM_KEY/LINEAR_TEAM_ID environment variable)"
312
+ )
313
+
314
+ # User email configuration (optional, for default assignee) - only in interactive mode
315
+ if interactive:
316
+ current_user_email = config_dict.get("user_email", "") if has_existing else ""
317
+
318
+ def prompt_user_email() -> str:
319
+ if current_user_email:
320
+ user_email_prompt = (
321
+ f"Your Linear email (optional, for auto-assignment) "
322
+ f"[current: {current_user_email}]"
323
+ )
324
+ return Prompt.ask(user_email_prompt, default=current_user_email)
325
+ return Prompt.ask(
326
+ "Your Linear email (optional, for auto-assignment)", default=""
327
+ )
328
+
329
+ def validate_user_email(email: str) -> tuple[bool, str | None]:
330
+ if not email: # Optional field
331
+ return True, None
332
+ import re
333
+
334
+ email_pattern = re.compile(
335
+ r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
336
+ )
337
+ if not email_pattern.match(email):
338
+ return False, f"Invalid email format: {email}"
339
+ return True, None
340
+
341
+ user_email = _retry_setting(
342
+ "User Email", prompt_user_email, validate_user_email
343
+ )
344
+ if user_email:
345
+ config_dict["user_email"] = user_email
346
+ console.print(f"[green]✓[/green] Will use {user_email} as default assignee")
347
+
348
+ # ============================================================
349
+ # DEFAULT VALUES SECTION (for ticket creation)
350
+ # ============================================================
351
+
352
+ console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
353
+ console.print("Configure default values for ticket creation:")
354
+
355
+ # Default epic/project
356
+ current_default_epic = (
357
+ config_dict.get("default_epic", "") if has_existing else ""
358
+ )
359
+
360
+ def prompt_default_epic() -> str:
361
+ if current_default_epic:
362
+ return Prompt.ask(
363
+ f"Default epic/project ID (optional) [current: {current_default_epic}]",
364
+ default=current_default_epic,
365
+ )
366
+ return Prompt.ask(
367
+ "Default epic/project ID (optional, accepts project URLs or IDs like 'PROJ-123')",
368
+ default="",
369
+ )
370
+
371
+ def validate_default_epic(epic_id: str) -> tuple[bool, str | None]:
372
+ if not epic_id: # Optional field
373
+ return True, None
374
+ # Basic validation - just check it's not empty when provided
375
+ if len(epic_id.strip()) < 2:
376
+ return False, "Epic/project ID must be at least 2 characters"
377
+ return True, None
378
+
379
+ default_epic = _retry_setting(
380
+ "Default Epic/Project", prompt_default_epic, validate_default_epic
381
+ )
382
+ if default_epic:
383
+ config_dict["default_epic"] = default_epic
384
+ config_dict["default_project"] = default_epic # Set both for compatibility
385
+ console.print(
386
+ f"[green]✓[/green] Will use '{default_epic}' as default epic/project"
387
+ )
388
+
389
+ # Default tags
390
+ current_default_tags = (
391
+ config_dict.get("default_tags", []) if has_existing else []
392
+ )
393
+
394
+ def prompt_default_tags() -> str:
395
+ if current_default_tags:
396
+ tags_str = ", ".join(current_default_tags)
397
+ return Prompt.ask(
398
+ f"Default tags (optional, comma-separated) [current: {tags_str}]",
399
+ default=tags_str,
400
+ )
401
+ return Prompt.ask(
402
+ "Default tags (optional, comma-separated, e.g., 'bug,urgent')",
403
+ default="",
404
+ )
405
+
406
+ def validate_default_tags(tags_input: str) -> tuple[bool, str | None]:
407
+ if not tags_input: # Optional field
408
+ return True, None
409
+ # Parse and validate tags
410
+ tags = [tag.strip() for tag in tags_input.split(",") if tag.strip()]
411
+ if not tags:
412
+ return False, "Please provide at least one tag or leave empty"
413
+ # Check each tag is reasonable
414
+ for tag in tags:
415
+ if len(tag) < 2:
416
+ return False, f"Tag '{tag}' must be at least 2 characters"
417
+ if len(tag) > 50:
418
+ return False, f"Tag '{tag}' is too long (max 50 characters)"
419
+ return True, None
420
+
421
+ default_tags_input = _retry_setting(
422
+ "Default Tags", prompt_default_tags, validate_default_tags
423
+ )
424
+ if default_tags_input:
425
+ default_tags = [
426
+ tag.strip() for tag in default_tags_input.split(",") if tag.strip()
427
+ ]
428
+ config_dict["default_tags"] = default_tags
429
+ console.print(f"[green]✓[/green] Will use tags: {', '.join(default_tags)}")
430
+
431
+ # Validate with detailed error reporting
155
432
  is_valid, error = ConfigValidator.validate_linear_config(config_dict)
433
+
156
434
  if not is_valid:
157
- console.print(f"[red]Configuration error: {error}[/red]")
435
+ console.print("\n[red]Configuration Validation Failed[/red]")
436
+ console.print(f"[red]Error: {error}[/red]\n")
437
+
438
+ # Show which settings were problematic
439
+ console.print("[yellow]Problematic settings:[/yellow]")
440
+ if "api_key" not in config_dict or not config_dict["api_key"]:
441
+ console.print(" • [red]API Key[/red] - Missing or empty")
442
+ if "team_key" not in config_dict and "team_id" not in config_dict:
443
+ console.print(
444
+ " • [red]Team Key/ID[/red] - Neither team_key nor team_id provided"
445
+ )
446
+ if "user_email" in config_dict:
447
+ email = config_dict["user_email"]
448
+ import re
449
+
450
+ if not re.match(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", email):
451
+ console.print(f" • [red]User Email[/red] - Invalid format: {email}")
452
+
453
+ # Offer to retry specific settings
454
+ console.print("\n[cyan]Options:[/cyan]")
455
+ console.print(" 1. Retry configuration from scratch")
456
+ console.print(" 2. Fix specific settings")
457
+ console.print(" 3. Exit")
458
+
459
+ choice = Prompt.ask("Choose an option", choices=["1", "2", "3"], default="2")
460
+
461
+ if choice == "1":
462
+ # Recursive retry
463
+ return _configure_linear(existing_config=None)
464
+ if choice == "2":
465
+ # Fix specific settings
466
+ return _configure_linear(existing_config=config_dict)
158
467
  raise typer.Exit(1) from None
159
468
 
160
- return AdapterConfig.from_dict(config_dict)
469
+ console.print("[green]✓ Configuration validated successfully[/green]")
470
+
471
+ # Extract default values to return separately (not part of AdapterConfig)
472
+ default_values = {}
473
+ if "user_email" in config_dict:
474
+ default_values["default_user"] = config_dict.pop("user_email")
475
+ if "default_epic" in config_dict:
476
+ default_values["default_epic"] = config_dict.pop("default_epic")
477
+ if "default_project" in config_dict:
478
+ default_values["default_project"] = config_dict.pop("default_project")
479
+ if "default_tags" in config_dict:
480
+ default_values["default_tags"] = config_dict.pop("default_tags")
161
481
 
482
+ return AdapterConfig.from_dict(config_dict), default_values
162
483
 
163
- def _configure_jira() -> AdapterConfig:
164
- """Configure JIRA adapter."""
165
- console.print("\n[bold]Configure JIRA Integration:[/bold]")
166
484
 
167
- # Server URL
168
- server = os.getenv("JIRA_SERVER") or ""
169
- if not server:
170
- server = Prompt.ask("JIRA Server URL (e.g., https://company.atlassian.net)")
485
+ def _configure_jira(
486
+ interactive: bool = True,
487
+ server: str | None = None,
488
+ email: str | None = None,
489
+ api_token: str | None = None,
490
+ project_key: str | None = None,
491
+ **kwargs: Any,
492
+ ) -> tuple[AdapterConfig, dict[str, Any]]:
493
+ """Configure JIRA adapter.
494
+
495
+ Supports both interactive (wizard) and programmatic (init command) modes.
496
+
497
+ Args:
498
+ ----
499
+ interactive: If True, prompt user for missing values (default: True)
500
+ server: Pre-provided JIRA server URL (optional)
501
+ email: Pre-provided JIRA user email (optional)
502
+ api_token: Pre-provided JIRA API token (optional)
503
+ project_key: Pre-provided default project key (optional)
504
+ **kwargs: Additional configuration parameters
505
+
506
+ Returns:
507
+ -------
508
+ Tuple of (AdapterConfig, default_values_dict)
509
+ - AdapterConfig: Configured JIRA adapter configuration
510
+ - default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
511
+
512
+ """
513
+ if interactive:
514
+ console.print("\n[bold]Configure JIRA Integration:[/bold]")
515
+
516
+ # Server URL (programmatic mode: use provided value or env, interactive: prompt)
517
+ final_server = server or os.getenv("JIRA_SERVER") or ""
518
+ if interactive and not final_server:
519
+ final_server = Prompt.ask(
520
+ "JIRA Server URL (e.g., https://company.atlassian.net)"
521
+ )
522
+ elif not interactive and not final_server:
523
+ raise ValueError(
524
+ "JIRA server URL is required (provide server parameter or set JIRA_SERVER environment variable)"
525
+ )
171
526
 
172
- # Email
173
- email = os.getenv("JIRA_EMAIL") or ""
174
- if not email:
175
- email = Prompt.ask("JIRA User Email")
527
+ # Email (programmatic mode: use provided value or env, interactive: prompt)
528
+ final_email = email or os.getenv("JIRA_EMAIL") or ""
529
+ if interactive and not final_email:
530
+ final_email = Prompt.ask("JIRA User Email")
531
+ elif not interactive and not final_email:
532
+ raise ValueError(
533
+ "JIRA email is required (provide email parameter or set JIRA_EMAIL environment variable)"
534
+ )
176
535
 
177
- # API Token
178
- api_token = os.getenv("JIRA_API_TOKEN") or ""
179
- if not api_token:
536
+ # API Token (programmatic mode: use provided value or env, interactive: prompt)
537
+ final_api_token = api_token or os.getenv("JIRA_API_TOKEN") or ""
538
+ if interactive and not final_api_token:
180
539
  console.print(
181
540
  "[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
182
541
  )
183
- api_token = Prompt.ask("JIRA API Token", password=True)
542
+ final_api_token = Prompt.ask("JIRA API Token", password=True)
543
+ elif not interactive and not final_api_token:
544
+ raise ValueError(
545
+ "JIRA API token is required (provide api_token parameter or set JIRA_API_TOKEN environment variable)"
546
+ )
184
547
 
185
- # Project Key
186
- project_key = Prompt.ask("Default Project Key (optional, e.g., PROJ)", default="")
548
+ # Project Key (optional)
549
+ final_project_key = project_key or os.getenv("JIRA_PROJECT_KEY") or ""
550
+ if interactive and not final_project_key:
551
+ final_project_key = Prompt.ask(
552
+ "Default Project Key (optional, e.g., PROJ)", default=""
553
+ )
187
554
 
188
555
  config_dict = {
189
556
  "adapter": AdapterType.JIRA.value,
190
- "server": server.rstrip("/"),
191
- "email": email,
192
- "api_token": api_token,
557
+ "server": final_server.rstrip("/"),
558
+ "email": final_email,
559
+ "api_token": final_api_token,
193
560
  }
194
561
 
195
- if project_key:
196
- config_dict["project_key"] = project_key
562
+ if final_project_key:
563
+ config_dict["project_key"] = final_project_key
197
564
 
198
565
  # Validate
199
566
  is_valid, error = ConfigValidator.validate_jira_config(config_dict)
200
567
  if not is_valid:
201
- console.print(f"[red]Configuration error: {error}[/red]")
202
- raise typer.Exit(1) from None
568
+ if interactive:
569
+ console.print(f"[red]Configuration error: {error}[/red]")
570
+ raise typer.Exit(1) from None
571
+ raise ValueError(f"JIRA configuration validation failed: {error}")
572
+
573
+ # ============================================================
574
+ # DEFAULT VALUES SECTION (for ticket creation)
575
+ # ============================================================
576
+ default_values = {}
577
+
578
+ if interactive:
579
+ console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
580
+ console.print("Configure default values for ticket creation:")
581
+
582
+ # Default user/assignee
583
+ user_input = Prompt.ask(
584
+ "Default assignee/user (optional, JIRA username or email)",
585
+ default="",
586
+ show_default=False,
587
+ )
588
+ if user_input:
589
+ default_values["default_user"] = user_input
590
+ console.print(
591
+ f"[green]✓[/green] Will use '{user_input}' as default assignee"
592
+ )
203
593
 
204
- return AdapterConfig.from_dict(config_dict)
594
+ # Default epic/project
595
+ epic_input = Prompt.ask(
596
+ "Default epic/project ID (optional, e.g., 'PROJ-123')",
597
+ default="",
598
+ show_default=False,
599
+ )
600
+ if epic_input:
601
+ default_values["default_epic"] = epic_input
602
+ default_values["default_project"] = epic_input # Compatibility
603
+ console.print(
604
+ f"[green]✓[/green] Will use '{epic_input}' as default epic/project"
605
+ )
205
606
 
607
+ # Default tags
608
+ tags_input = Prompt.ask(
609
+ "Default tags/labels (optional, comma-separated, e.g., 'bug,urgent')",
610
+ default="",
611
+ show_default=False,
612
+ )
613
+ if tags_input:
614
+ tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
615
+ if tags_list:
616
+ default_values["default_tags"] = tags_list
617
+ console.print(f"[green]✓[/green] Will use tags: {', '.join(tags_list)}")
206
618
 
207
- def _configure_github() -> AdapterConfig:
208
- """Configure GitHub adapter."""
209
- console.print("\n[bold]Configure GitHub Integration:[/bold]")
619
+ return AdapterConfig.from_dict(config_dict), default_values
210
620
 
211
- # Token
212
- token = os.getenv("GITHUB_TOKEN") or ""
213
- if token:
214
- console.print("[dim]Found GITHUB_TOKEN in environment[/dim]")
215
- use_env = Confirm.ask("Use this token?", default=True)
216
- if not use_env:
217
- token = ""
218
621
 
219
- if not token:
220
- console.print(
221
- "[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
622
+ def _configure_github(
623
+ interactive: bool = True,
624
+ token: str | None = None,
625
+ repo_url: str | None = None,
626
+ owner: str | None = None,
627
+ repo: str | None = None,
628
+ **kwargs: Any,
629
+ ) -> tuple[AdapterConfig, dict[str, Any]]:
630
+ """Configure GitHub adapter.
631
+
632
+ Supports both interactive (wizard) and programmatic (init command) modes.
633
+
634
+ Args:
635
+ ----
636
+ interactive: If True, prompt user for missing values (default: True)
637
+ token: Pre-provided GitHub Personal Access Token (optional)
638
+ repo_url: Pre-provided GitHub repository URL (optional, preferred)
639
+ owner: Pre-provided repository owner (optional, fallback)
640
+ repo: Pre-provided repository name (optional, fallback)
641
+ **kwargs: Additional configuration parameters
642
+
643
+ Returns:
644
+ -------
645
+ Tuple of (AdapterConfig, default_values_dict)
646
+ - AdapterConfig: Configured GitHub adapter configuration
647
+ - default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
648
+
649
+ """
650
+ if interactive:
651
+ console.print("\n[bold]Configure GitHub Integration:[/bold]")
652
+
653
+ # Token (programmatic mode: use provided value or env, interactive: prompt)
654
+ final_token = token or os.getenv("GITHUB_TOKEN") or ""
655
+ if interactive:
656
+ if final_token:
657
+ console.print("[dim]Found GITHUB_TOKEN in environment[/dim]")
658
+ use_env = Confirm.ask("Use this token?", default=True)
659
+ if not use_env:
660
+ final_token = ""
661
+
662
+ if not final_token:
663
+ console.print(
664
+ "[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
665
+ )
666
+ console.print(
667
+ "[dim]Required scopes: repo (or public_repo for public repos)[/dim]"
668
+ )
669
+ final_token = Prompt.ask("GitHub Personal Access Token", password=True)
670
+ elif not final_token:
671
+ raise ValueError(
672
+ "GitHub token is required (provide token parameter or set GITHUB_TOKEN environment variable)"
222
673
  )
674
+
675
+ # Repository URL/Owner/Repo - Prioritize repo_url, fallback to owner/repo
676
+ # Programmatic mode: use repo_url or GITHUB_REPO_URL env, fallback to owner/repo
677
+ final_owner = ""
678
+ final_repo = ""
679
+
680
+ # Step 1: Try to get URL from parameter or environment
681
+ url_input = repo_url or os.getenv("GITHUB_REPO_URL") or ""
682
+
683
+ # Step 2: Parse URL if provided
684
+ if url_input:
685
+ from ..core.url_parser import parse_github_repo_url
686
+
687
+ parsed_owner, parsed_repo, error = parse_github_repo_url(url_input)
688
+ if parsed_owner and parsed_repo:
689
+ final_owner = parsed_owner
690
+ final_repo = parsed_repo
691
+ if interactive:
692
+ console.print(
693
+ f"[dim]✓ Extracted repository: {final_owner}/{final_repo}[/dim]"
694
+ )
695
+ else:
696
+ # URL parsing failed
697
+ if interactive:
698
+ console.print(f"[yellow]Warning: {error}[/yellow]")
699
+ else:
700
+ raise ValueError(f"Failed to parse GitHub repository URL: {error}")
701
+
702
+ # Step 3: Interactive mode - prompt for URL if not provided
703
+ if interactive and not final_owner and not final_repo:
223
704
  console.print(
224
- "[dim]Required scopes: repo (or public_repo for public repos)[/dim]"
705
+ "[dim]Enter your GitHub repository URL (e.g., https://github.com/owner/repo)[/dim]"
225
706
  )
226
- token = Prompt.ask("GitHub Personal Access Token", password=True)
227
707
 
228
- # Repository Owner
229
- owner = os.getenv("GITHUB_OWNER") or ""
230
- if not owner:
231
- owner = Prompt.ask("Repository Owner (username or org)")
708
+ # Keep prompting until we get a valid URL
709
+ while not final_owner or not final_repo:
710
+ from ..core.url_parser import parse_github_repo_url
711
+
712
+ url_prompt = Prompt.ask(
713
+ "GitHub Repository URL",
714
+ default="https://github.com/",
715
+ )
716
+
717
+ parsed_owner, parsed_repo, error = parse_github_repo_url(url_prompt)
718
+ if parsed_owner and parsed_repo:
719
+ final_owner = parsed_owner
720
+ final_repo = parsed_repo
721
+ console.print(f"[dim]✓ Repository: {final_owner}/{final_repo}[/dim]")
722
+ break
723
+ else:
724
+ console.print(f"[red]Error: {error}[/red]")
725
+ console.print(
726
+ "[yellow]Please enter a valid GitHub repository URL[/yellow]"
727
+ )
232
728
 
233
- # Repository Name
234
- repo = os.getenv("GITHUB_REPO") or ""
235
- if not repo:
236
- repo = Prompt.ask("Repository Name")
729
+ # Step 4: Non-interactive fallback - use individual owner/repo parameters
730
+ if not final_owner or not final_repo:
731
+ fallback_owner = owner or os.getenv("GITHUB_OWNER") or ""
732
+ fallback_repo = repo or os.getenv("GITHUB_REPO") or ""
733
+
734
+ # In non-interactive mode, both must be provided if URL wasn't
735
+ if not interactive:
736
+ if not fallback_owner or not fallback_repo:
737
+ raise ValueError(
738
+ "GitHub repository is required. Provide either:\n"
739
+ " - repo_url parameter or GITHUB_REPO_URL environment variable, OR\n"
740
+ " - Both owner and repo parameters (or GITHUB_OWNER/GITHUB_REPO environment variables)"
741
+ )
742
+ final_owner = fallback_owner
743
+ final_repo = fallback_repo
744
+ else:
745
+ # Interactive mode with fallback values
746
+ if fallback_owner:
747
+ final_owner = fallback_owner
748
+ if fallback_repo:
749
+ final_repo = fallback_repo
237
750
 
238
751
  config_dict = {
239
752
  "adapter": AdapterType.GITHUB.value,
240
- "token": token,
241
- "owner": owner,
242
- "repo": repo,
243
- "project_id": f"{owner}/{repo}", # Convenience field
753
+ "token": final_token,
754
+ "owner": final_owner,
755
+ "repo": final_repo,
756
+ "project_id": f"{final_owner}/{final_repo}", # Convenience field
244
757
  }
245
758
 
246
759
  # Validate
247
760
  is_valid, error = ConfigValidator.validate_github_config(config_dict)
248
761
  if not is_valid:
249
- console.print(f"[red]Configuration error: {error}[/red]")
250
- raise typer.Exit(1) from None
762
+ if interactive:
763
+ console.print(f"[red]Configuration error: {error}[/red]")
764
+ raise typer.Exit(1) from None
765
+ raise ValueError(f"GitHub configuration validation failed: {error}")
766
+
767
+ # ============================================================
768
+ # DEFAULT VALUES SECTION (for ticket creation)
769
+ # ============================================================
770
+ default_values = {}
771
+
772
+ if interactive:
773
+ console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
774
+ console.print("Configure default values for ticket creation:")
775
+
776
+ # Default user/assignee
777
+ user_input = Prompt.ask(
778
+ "Default assignee/user (optional, GitHub username)",
779
+ default="",
780
+ show_default=False,
781
+ )
782
+ if user_input:
783
+ default_values["default_user"] = user_input
784
+ console.print(
785
+ f"[green]✓[/green] Will use '{user_input}' as default assignee"
786
+ )
251
787
 
252
- return AdapterConfig.from_dict(config_dict)
788
+ # Default epic/project (milestone for GitHub)
789
+ epic_input = Prompt.ask(
790
+ "Default milestone/project (optional, e.g., 'v1.0' or milestone number)",
791
+ default="",
792
+ show_default=False,
793
+ )
794
+ if epic_input:
795
+ default_values["default_epic"] = epic_input
796
+ default_values["default_project"] = epic_input # Compatibility
797
+ console.print(
798
+ f"[green]✓[/green] Will use '{epic_input}' as default milestone/project"
799
+ )
253
800
 
801
+ # Default tags (labels for GitHub)
802
+ tags_input = Prompt.ask(
803
+ "Default labels (optional, comma-separated, e.g., 'bug,enhancement')",
804
+ default="",
805
+ show_default=False,
806
+ )
807
+ if tags_input:
808
+ tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
809
+ if tags_list:
810
+ default_values["default_tags"] = tags_list
811
+ console.print(
812
+ f"[green]✓[/green] Will use labels: {', '.join(tags_list)}"
813
+ )
254
814
 
255
- def _configure_aitrackdown() -> AdapterConfig:
256
- """Configure AITrackdown adapter."""
257
- console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
815
+ return AdapterConfig.from_dict(config_dict), default_values
258
816
 
259
- base_path = Prompt.ask("Base path for ticket storage", default=".aitrackdown")
817
+
818
+ def _configure_aitrackdown(
819
+ interactive: bool = True, base_path: str | None = None, **kwargs: Any
820
+ ) -> tuple[AdapterConfig, dict[str, Any]]:
821
+ """Configure AITrackdown adapter.
822
+
823
+ Supports both interactive (wizard) and programmatic (init command) modes.
824
+
825
+ Args:
826
+ ----
827
+ interactive: If True, prompt user for missing values (default: True)
828
+ base_path: Pre-provided base path for ticket storage (optional)
829
+ **kwargs: Additional configuration parameters
830
+
831
+ Returns:
832
+ -------
833
+ Tuple of (AdapterConfig, default_values_dict)
834
+ - AdapterConfig: Configured AITrackdown adapter configuration
835
+ - default_values_dict: Dictionary containing default_user, default_epic, default_project, default_tags
836
+
837
+ """
838
+ if interactive:
839
+ console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
840
+
841
+ # Base path (programmatic mode: use provided value or default, interactive: prompt)
842
+ final_base_path = base_path or ".aitrackdown"
843
+ if interactive:
844
+ final_base_path = Prompt.ask(
845
+ "Base path for ticket storage", default=".aitrackdown"
846
+ )
260
847
 
261
848
  config_dict = {
262
849
  "adapter": AdapterType.AITRACKDOWN.value,
263
- "base_path": base_path,
850
+ "base_path": final_base_path,
264
851
  }
265
852
 
266
- return AdapterConfig.from_dict(config_dict)
853
+ # ============================================================
854
+ # DEFAULT VALUES SECTION (for ticket creation)
855
+ # ============================================================
856
+ default_values = {}
857
+
858
+ if interactive:
859
+ console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
860
+ console.print("Configure default values for ticket creation:")
861
+
862
+ # Default user/assignee
863
+ user_input = Prompt.ask(
864
+ "Default assignee/user (optional)", default="", show_default=False
865
+ )
866
+ if user_input:
867
+ default_values["default_user"] = user_input
868
+ console.print(
869
+ f"[green]✓[/green] Will use '{user_input}' as default assignee"
870
+ )
871
+
872
+ # Default epic/project
873
+ epic_input = Prompt.ask(
874
+ "Default epic/project ID (optional)", default="", show_default=False
875
+ )
876
+ if epic_input:
877
+ default_values["default_epic"] = epic_input
878
+ default_values["default_project"] = epic_input # Compatibility
879
+ console.print(
880
+ f"[green]✓[/green] Will use '{epic_input}' as default epic/project"
881
+ )
882
+
883
+ # Default tags
884
+ tags_input = Prompt.ask(
885
+ "Default tags (optional, comma-separated)", default="", show_default=False
886
+ )
887
+ if tags_input:
888
+ tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
889
+ if tags_list:
890
+ default_values["default_tags"] = tags_list
891
+ console.print(f"[green]✓[/green] Will use tags: {', '.join(tags_list)}")
892
+
893
+ return AdapterConfig.from_dict(config_dict), default_values
894
+
895
+
896
+ def prompt_default_values(
897
+ adapter_type: str,
898
+ existing_values: dict[str, Any] | None = None,
899
+ ) -> dict[str, Any]:
900
+ """Prompt user for default values (for ticket creation).
901
+
902
+ This is a standalone function that can be called independently of adapter configuration.
903
+ Used when adapter credentials exist but default values need to be set or updated.
904
+
905
+ Args:
906
+ ----
907
+ adapter_type: Type of adapter (linear, jira, github, aitrackdown)
908
+ existing_values: Optional existing default values to show as current values
909
+
910
+ Returns:
911
+ -------
912
+ Dictionary containing default_user, default_epic, default_project, default_tags
913
+ (only includes keys that were provided by the user)
914
+
915
+ """
916
+ console.print("\n[bold cyan]Default Values (Optional)[/bold cyan]")
917
+ console.print("Configure default values for ticket creation:")
918
+
919
+ default_values = {}
920
+ existing_values = existing_values or {}
921
+
922
+ # Default user/assignee
923
+ current_user = existing_values.get("default_user", "")
924
+ if current_user:
925
+ user_input = Prompt.ask(
926
+ f"Default assignee/user (optional) [current: {current_user}]",
927
+ default=current_user,
928
+ show_default=False,
929
+ )
930
+ else:
931
+ user_input = Prompt.ask(
932
+ "Default assignee/user (optional)",
933
+ default="",
934
+ show_default=False,
935
+ )
936
+ if user_input:
937
+ default_values["default_user"] = user_input
938
+ console.print(f"[green]✓[/green] Will use '{user_input}' as default assignee")
939
+
940
+ # Default epic/project
941
+ current_epic = existing_values.get("default_epic") or existing_values.get(
942
+ "default_project", ""
943
+ )
944
+
945
+ # Adapter-specific messaging
946
+ if adapter_type == "github":
947
+ epic_label = "milestone/project"
948
+ epic_example = "e.g., 'v1.0' or milestone number"
949
+ elif adapter_type == "linear":
950
+ epic_label = "epic/project ID"
951
+ epic_example = "e.g., 'PROJ-123' or UUID"
952
+ else:
953
+ epic_label = "epic/project ID"
954
+ epic_example = "e.g., 'PROJ-123'"
955
+
956
+ if current_epic:
957
+ epic_input = Prompt.ask(
958
+ f"Default {epic_label} (optional) [current: {current_epic}]",
959
+ default=current_epic,
960
+ show_default=False,
961
+ )
962
+ else:
963
+ epic_input = Prompt.ask(
964
+ f"Default {epic_label} (optional, {epic_example})",
965
+ default="",
966
+ show_default=False,
967
+ )
968
+ if epic_input:
969
+ default_values["default_epic"] = epic_input
970
+ default_values["default_project"] = epic_input # Compatibility
971
+ console.print(
972
+ f"[green]✓[/green] Will use '{epic_input}' as default {epic_label}"
973
+ )
974
+
975
+ # Default tags
976
+ current_tags = existing_values.get("default_tags", [])
977
+ current_tags_str = ", ".join(current_tags) if current_tags else ""
978
+
979
+ # Adapter-specific messaging
980
+ tags_label = "labels" if adapter_type == "github" else "tags/labels"
981
+
982
+ if current_tags_str:
983
+ tags_input = Prompt.ask(
984
+ f"Default {tags_label} (optional, comma-separated) [current: {current_tags_str}]",
985
+ default=current_tags_str,
986
+ show_default=False,
987
+ )
988
+ else:
989
+ tags_input = Prompt.ask(
990
+ f"Default {tags_label} (optional, comma-separated, e.g., 'bug,urgent')",
991
+ default="",
992
+ show_default=False,
993
+ )
994
+ if tags_input:
995
+ tags_list = [t.strip() for t in tags_input.split(",") if t.strip()]
996
+ if tags_list:
997
+ default_values["default_tags"] = tags_list
998
+ console.print(
999
+ f"[green]✓[/green] Will use {tags_label}: {', '.join(tags_list)}"
1000
+ )
1001
+
1002
+ return default_values
267
1003
 
268
1004
 
269
1005
  def _configure_hybrid_mode() -> TicketerConfig:
@@ -301,20 +1037,25 @@ def _configure_hybrid_mode() -> TicketerConfig:
301
1037
 
302
1038
  # Configure each adapter
303
1039
  adapters = {}
1040
+ default_values: dict[str, str] = {}
304
1041
  for adapter_type in selected_adapters:
305
1042
  console.print(f"\n[cyan]Configuring {adapter_type.value}...[/cyan]")
306
1043
 
307
1044
  if adapter_type == AdapterType.LINEAR:
308
- adapter_config = _configure_linear()
1045
+ adapter_config, adapter_defaults = _configure_linear(interactive=True)
309
1046
  elif adapter_type == AdapterType.JIRA:
310
- adapter_config = _configure_jira()
1047
+ adapter_config, adapter_defaults = _configure_jira(interactive=True)
311
1048
  elif adapter_type == AdapterType.GITHUB:
312
- adapter_config = _configure_github()
1049
+ adapter_config, adapter_defaults = _configure_github(interactive=True)
313
1050
  else:
314
- adapter_config = _configure_aitrackdown()
1051
+ adapter_config, adapter_defaults = _configure_aitrackdown(interactive=True)
315
1052
 
316
1053
  adapters[adapter_type.value] = adapter_config
317
1054
 
1055
+ # Only save defaults from the first/primary adapter
1056
+ if not default_values:
1057
+ default_values = adapter_defaults
1058
+
318
1059
  # Select primary adapter
319
1060
  console.print("\n[bold]Select primary adapter (source of truth):[/bold]")
320
1061
  for idx, adapter_type in enumerate(selected_adapters, 1):
@@ -354,9 +1095,15 @@ def _configure_hybrid_mode() -> TicketerConfig:
354
1095
  sync_strategy=sync_strategy,
355
1096
  )
356
1097
 
357
- # Create full config
1098
+ # Create full config with default values
358
1099
  config = TicketerConfig(
359
- default_adapter=primary_adapter, adapters=adapters, hybrid_mode=hybrid_config
1100
+ default_adapter=primary_adapter,
1101
+ adapters=adapters,
1102
+ hybrid_mode=hybrid_config,
1103
+ default_user=default_values.get("default_user"),
1104
+ default_project=default_values.get("default_project"),
1105
+ default_epic=default_values.get("default_epic"),
1106
+ default_tags=default_values.get("default_tags"),
360
1107
  )
361
1108
 
362
1109
  return config
@@ -437,6 +1184,7 @@ def set_adapter_config(
437
1184
  """Set specific adapter configuration values.
438
1185
 
439
1186
  Args:
1187
+ ----
440
1188
  adapter: Adapter type to set as default
441
1189
  api_key: API key/token
442
1190
  project_id: Project ID