mcp-ticketer 0.1.22__py3-none-any.whl → 0.1.24__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 (33) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +1 -1
  3. mcp_ticketer/adapters/aitrackdown.py +15 -14
  4. mcp_ticketer/adapters/github.py +21 -20
  5. mcp_ticketer/adapters/hybrid.py +13 -12
  6. mcp_ticketer/adapters/jira.py +32 -27
  7. mcp_ticketer/adapters/linear.py +29 -26
  8. mcp_ticketer/cache/memory.py +2 -2
  9. mcp_ticketer/cli/auggie_configure.py +237 -0
  10. mcp_ticketer/cli/codex_configure.py +257 -0
  11. mcp_ticketer/cli/gemini_configure.py +261 -0
  12. mcp_ticketer/cli/main.py +171 -10
  13. mcp_ticketer/cli/migrate_config.py +3 -7
  14. mcp_ticketer/cli/utils.py +8 -8
  15. mcp_ticketer/core/adapter.py +12 -11
  16. mcp_ticketer/core/config.py +17 -17
  17. mcp_ticketer/core/env_discovery.py +24 -24
  18. mcp_ticketer/core/http_client.py +13 -13
  19. mcp_ticketer/core/mappers.py +25 -25
  20. mcp_ticketer/core/models.py +10 -10
  21. mcp_ticketer/core/project_config.py +25 -22
  22. mcp_ticketer/core/registry.py +7 -7
  23. mcp_ticketer/mcp/server.py +18 -18
  24. mcp_ticketer/queue/manager.py +2 -2
  25. mcp_ticketer/queue/queue.py +7 -7
  26. mcp_ticketer/queue/worker.py +8 -8
  27. {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/METADATA +58 -8
  28. mcp_ticketer-0.1.24.dist-info/RECORD +45 -0
  29. mcp_ticketer-0.1.22.dist-info/RECORD +0 -42
  30. {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/WHEEL +0 -0
  31. {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/entry_points.txt +0 -0
  32. {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/licenses/LICENSE +0 -0
  33. {mcp_ticketer-0.1.22.dist-info → mcp_ticketer-0.1.24.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ environment files, including:
11
11
  import logging
12
12
  from dataclasses import dataclass, field
13
13
  from pathlib import Path
14
- from typing import Any, Dict, List, Optional
14
+ from typing import Any, Optional
15
15
 
16
16
  from dotenv import dotenv_values
17
17
 
@@ -105,9 +105,9 @@ class DiscoveredAdapter:
105
105
  """Information about a discovered adapter configuration."""
106
106
 
107
107
  adapter_type: str
108
- config: Dict[str, Any]
108
+ config: dict[str, Any]
109
109
  confidence: float # 0.0-1.0 how complete the configuration is
110
- missing_fields: List[str] = field(default_factory=list)
110
+ missing_fields: list[str] = field(default_factory=list)
111
111
  found_in: str = ".env" # Which file it was found in
112
112
 
113
113
  def is_complete(self) -> bool:
@@ -119,9 +119,9 @@ class DiscoveredAdapter:
119
119
  class DiscoveryResult:
120
120
  """Result of environment file discovery."""
121
121
 
122
- adapters: List[DiscoveredAdapter] = field(default_factory=list)
123
- warnings: List[str] = field(default_factory=list)
124
- env_files_found: List[str] = field(default_factory=list)
122
+ adapters: list[DiscoveredAdapter] = field(default_factory=list)
123
+ warnings: list[str] = field(default_factory=list)
124
+ env_files_found: list[str] = field(default_factory=list)
125
125
 
126
126
  def get_primary_adapter(self) -> Optional[DiscoveredAdapter]:
127
127
  """Get the adapter with highest confidence and completeness."""
@@ -209,7 +209,7 @@ class EnvDiscovery:
209
209
 
210
210
  return result
211
211
 
212
- def _load_env_files(self, result: DiscoveryResult) -> Dict[str, str]:
212
+ def _load_env_files(self, result: DiscoveryResult) -> dict[str, str]:
213
213
  """Load environment variables from files.
214
214
 
215
215
  Args:
@@ -219,7 +219,7 @@ class EnvDiscovery:
219
219
  Merged dictionary of environment variables
220
220
 
221
221
  """
222
- merged_env: Dict[str, str] = {}
222
+ merged_env: dict[str, str] = {}
223
223
 
224
224
  # Load files in reverse order (lowest priority first)
225
225
  for env_file in reversed(self.ENV_FILE_ORDER):
@@ -239,7 +239,7 @@ class EnvDiscovery:
239
239
  return merged_env
240
240
 
241
241
  def _find_key_value(
242
- self, env_vars: Dict[str, str], patterns: List[str]
242
+ self, env_vars: dict[str, str], patterns: list[str]
243
243
  ) -> Optional[str]:
244
244
  """Find first matching key value from patterns.
245
245
 
@@ -257,7 +257,7 @@ class EnvDiscovery:
257
257
  return None
258
258
 
259
259
  def _detect_linear(
260
- self, env_vars: Dict[str, str], found_in: str
260
+ self, env_vars: dict[str, str], found_in: str
261
261
  ) -> Optional[DiscoveredAdapter]:
262
262
  """Detect Linear adapter configuration.
263
263
 
@@ -274,12 +274,12 @@ class EnvDiscovery:
274
274
  if not api_key:
275
275
  return None
276
276
 
277
- config: Dict[str, Any] = {
277
+ config: dict[str, Any] = {
278
278
  "api_key": api_key,
279
279
  "adapter": AdapterType.LINEAR.value,
280
280
  }
281
281
 
282
- missing_fields: List[str] = []
282
+ missing_fields: list[str] = []
283
283
  confidence = 0.6 # Has API key
284
284
 
285
285
  # Extract team ID (recommended but not required)
@@ -305,7 +305,7 @@ class EnvDiscovery:
305
305
  )
306
306
 
307
307
  def _detect_github(
308
- self, env_vars: Dict[str, str], found_in: str
308
+ self, env_vars: dict[str, str], found_in: str
309
309
  ) -> Optional[DiscoveredAdapter]:
310
310
  """Detect GitHub adapter configuration.
311
311
 
@@ -322,12 +322,12 @@ class EnvDiscovery:
322
322
  if not token:
323
323
  return None
324
324
 
325
- config: Dict[str, Any] = {
325
+ config: dict[str, Any] = {
326
326
  "token": token,
327
327
  "adapter": AdapterType.GITHUB.value,
328
328
  }
329
329
 
330
- missing_fields: List[str] = []
330
+ missing_fields: list[str] = []
331
331
  confidence = 0.4 # Has token
332
332
 
333
333
  # Try to extract owner/repo from combined field
@@ -364,7 +364,7 @@ class EnvDiscovery:
364
364
  )
365
365
 
366
366
  def _detect_jira(
367
- self, env_vars: Dict[str, str], found_in: str
367
+ self, env_vars: dict[str, str], found_in: str
368
368
  ) -> Optional[DiscoveredAdapter]:
369
369
  """Detect JIRA adapter configuration.
370
370
 
@@ -381,12 +381,12 @@ class EnvDiscovery:
381
381
  if not api_token:
382
382
  return None
383
383
 
384
- config: Dict[str, Any] = {
384
+ config: dict[str, Any] = {
385
385
  "api_token": api_token,
386
386
  "adapter": AdapterType.JIRA.value,
387
387
  }
388
388
 
389
- missing_fields: List[str] = []
389
+ missing_fields: list[str] = []
390
390
  confidence = 0.3 # Has token
391
391
 
392
392
  # Extract server (required)
@@ -420,7 +420,7 @@ class EnvDiscovery:
420
420
  )
421
421
 
422
422
  def _detect_aitrackdown(
423
- self, env_vars: Dict[str, str], found_in: str
423
+ self, env_vars: dict[str, str], found_in: str
424
424
  ) -> Optional[DiscoveredAdapter]:
425
425
  """Detect AITrackdown adapter configuration.
426
426
 
@@ -439,7 +439,7 @@ class EnvDiscovery:
439
439
  if not base_path and not aitrackdown_dir.exists():
440
440
  return None
441
441
 
442
- config: Dict[str, Any] = {
442
+ config: dict[str, Any] = {
443
443
  "adapter": AdapterType.AITRACKDOWN.value,
444
444
  }
445
445
 
@@ -459,14 +459,14 @@ class EnvDiscovery:
459
459
  found_in=found_in,
460
460
  )
461
461
 
462
- def _validate_security(self) -> List[str]:
462
+ def _validate_security(self) -> list[str]:
463
463
  """Validate security of environment files.
464
464
 
465
465
  Returns:
466
466
  List of security warnings
467
467
 
468
468
  """
469
- warnings: List[str] = []
469
+ warnings: list[str] = []
470
470
 
471
471
  # Check if .env files are tracked in git
472
472
  gitignore_path = self.project_path / ".gitignore"
@@ -524,7 +524,7 @@ class EnvDiscovery:
524
524
  logger.debug(f"Git check failed: {e}")
525
525
  return False
526
526
 
527
- def validate_discovered_config(self, adapter: DiscoveredAdapter) -> List[str]:
527
+ def validate_discovered_config(self, adapter: DiscoveredAdapter) -> list[str]:
528
528
  """Validate a discovered adapter configuration.
529
529
 
530
530
  Args:
@@ -534,7 +534,7 @@ class EnvDiscovery:
534
534
  List of validation warnings
535
535
 
536
536
  """
537
- warnings: List[str] = []
537
+ warnings: list[str] = []
538
538
 
539
539
  # Check API key/token length (basic sanity check)
540
540
  if adapter.adapter_type == AdapterType.LINEAR.value:
@@ -4,7 +4,7 @@ import asyncio
4
4
  import logging
5
5
  import time
6
6
  from enum import Enum
7
- from typing import Any, Dict, List, Optional, Union
7
+ from typing import Any, Optional, Union
8
8
 
9
9
  import httpx
10
10
  from httpx import AsyncClient, TimeoutException
@@ -32,8 +32,8 @@ class RetryConfig:
32
32
  max_delay: float = 60.0,
33
33
  exponential_base: float = 2.0,
34
34
  jitter: bool = True,
35
- retry_on_status: Optional[List[int]] = None,
36
- retry_on_exceptions: Optional[List[type]] = None,
35
+ retry_on_status: Optional[list[int]] = None,
36
+ retry_on_exceptions: Optional[list[type]] = None,
37
37
  ):
38
38
  self.max_retries = max_retries
39
39
  self.initial_delay = initial_delay
@@ -94,7 +94,7 @@ class BaseHTTPClient:
94
94
  def __init__(
95
95
  self,
96
96
  base_url: str,
97
- headers: Optional[Dict[str, str]] = None,
97
+ headers: Optional[dict[str, str]] = None,
98
98
  auth: Optional[Union[httpx.Auth, tuple]] = None,
99
99
  timeout: float = 30.0,
100
100
  retry_config: Optional[RetryConfig] = None,
@@ -200,10 +200,10 @@ class BaseHTTPClient:
200
200
  self,
201
201
  method: Union[HTTPMethod, str],
202
202
  endpoint: str,
203
- data: Optional[Dict[str, Any]] = None,
204
- json: Optional[Dict[str, Any]] = None,
205
- params: Optional[Dict[str, Any]] = None,
206
- headers: Optional[Dict[str, str]] = None,
203
+ data: Optional[dict[str, Any]] = None,
204
+ json: Optional[dict[str, Any]] = None,
205
+ params: Optional[dict[str, Any]] = None,
206
+ headers: Optional[dict[str, str]] = None,
207
207
  timeout: Optional[float] = None,
208
208
  retry_count: int = 0,
209
209
  **kwargs,
@@ -313,7 +313,7 @@ class BaseHTTPClient:
313
313
  """Make DELETE request."""
314
314
  return await self.request(HTTPMethod.DELETE, endpoint, **kwargs)
315
315
 
316
- async def get_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
316
+ async def get_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
317
317
  """Make GET request and return JSON response."""
318
318
  response = await self.get(endpoint, **kwargs)
319
319
 
@@ -323,7 +323,7 @@ class BaseHTTPClient:
323
323
 
324
324
  return response.json()
325
325
 
326
- async def post_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
326
+ async def post_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
327
327
  """Make POST request and return JSON response."""
328
328
  response = await self.post(endpoint, **kwargs)
329
329
 
@@ -333,7 +333,7 @@ class BaseHTTPClient:
333
333
 
334
334
  return response.json()
335
335
 
336
- async def put_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
336
+ async def put_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
337
337
  """Make PUT request and return JSON response."""
338
338
  response = await self.put(endpoint, **kwargs)
339
339
 
@@ -343,7 +343,7 @@ class BaseHTTPClient:
343
343
 
344
344
  return response.json()
345
345
 
346
- async def patch_json(self, endpoint: str, **kwargs) -> Dict[str, Any]:
346
+ async def patch_json(self, endpoint: str, **kwargs) -> dict[str, Any]:
347
347
  """Make PATCH request and return JSON response."""
348
348
  response = await self.patch(endpoint, **kwargs)
349
349
 
@@ -353,7 +353,7 @@ class BaseHTTPClient:
353
353
 
354
354
  return response.json()
355
355
 
356
- def get_stats(self) -> Dict[str, Any]:
356
+ def get_stats(self) -> dict[str, Any]:
357
357
  """Get client statistics."""
358
358
  return self.stats.copy()
359
359
 
@@ -3,7 +3,7 @@
3
3
  import logging
4
4
  from abc import ABC, abstractmethod
5
5
  from functools import lru_cache
6
- from typing import Any, Dict, Generic, List, Optional, TypeVar
6
+ from typing import Any, Generic, Optional, TypeVar
7
7
 
8
8
  from .models import Priority, TicketState
9
9
 
@@ -16,16 +16,16 @@ U = TypeVar("U")
16
16
  class BiDirectionalDict(Generic[T, U]):
17
17
  """Bidirectional dictionary for efficient lookups in both directions."""
18
18
 
19
- def __init__(self, mapping: Dict[T, U]):
19
+ def __init__(self, mapping: dict[T, U]):
20
20
  """Initialize with forward mapping.
21
21
 
22
22
  Args:
23
23
  mapping: Forward mapping dictionary
24
24
 
25
25
  """
26
- self._forward: Dict[T, U] = mapping.copy()
27
- self._reverse: Dict[U, T] = {v: k for k, v in mapping.items()}
28
- self._cache: Dict[str, Any] = {}
26
+ self._forward: dict[T, U] = mapping.copy()
27
+ self._reverse: dict[U, T] = {v: k for k, v in mapping.items()}
28
+ self._cache: dict[str, Any] = {}
29
29
 
30
30
  def get_forward(self, key: T, default: Optional[U] = None) -> Optional[U]:
31
31
  """Get value by forward key."""
@@ -43,15 +43,15 @@ class BiDirectionalDict(Generic[T, U]):
43
43
  """Check if reverse key exists."""
44
44
  return key in self._reverse
45
45
 
46
- def forward_keys(self) -> List[T]:
46
+ def forward_keys(self) -> list[T]:
47
47
  """Get all forward keys."""
48
48
  return list(self._forward.keys())
49
49
 
50
- def reverse_keys(self) -> List[U]:
50
+ def reverse_keys(self) -> list[U]:
51
51
  """Get all reverse keys."""
52
52
  return list(self._reverse.keys())
53
53
 
54
- def items(self) -> List[tuple[T, U]]:
54
+ def items(self) -> list[tuple[T, U]]:
55
55
  """Get all key-value pairs."""
56
56
  return list(self._forward.items())
57
57
 
@@ -67,7 +67,7 @@ class BaseMapper(ABC):
67
67
 
68
68
  """
69
69
  self.cache_size = cache_size
70
- self._cache: Dict[str, Any] = {}
70
+ self._cache: dict[str, Any] = {}
71
71
 
72
72
  @abstractmethod
73
73
  def get_mapping(self) -> BiDirectionalDict:
@@ -83,7 +83,7 @@ class StateMapper(BaseMapper):
83
83
  """Universal state mapping utility."""
84
84
 
85
85
  def __init__(
86
- self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
86
+ self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
87
87
  ):
88
88
  """Initialize state mapper.
89
89
 
@@ -221,7 +221,7 @@ class StateMapper(BaseMapper):
221
221
  self._cache[cache_key] = result
222
222
  return result
223
223
 
224
- def get_available_states(self) -> List[str]:
224
+ def get_available_states(self) -> list[str]:
225
225
  """Get all available adapter states."""
226
226
  return self.get_mapping().reverse_keys()
227
227
 
@@ -258,7 +258,7 @@ class PriorityMapper(BaseMapper):
258
258
  """Universal priority mapping utility."""
259
259
 
260
260
  def __init__(
261
- self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
261
+ self, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
262
262
  ):
263
263
  """Initialize priority mapper.
264
264
 
@@ -418,11 +418,11 @@ class PriorityMapper(BaseMapper):
418
418
  self._cache[cache_key] = result
419
419
  return result
420
420
 
421
- def get_available_priorities(self) -> List[Any]:
421
+ def get_available_priorities(self) -> list[Any]:
422
422
  """Get all available adapter priorities."""
423
423
  return self.get_mapping().reverse_keys()
424
424
 
425
- def get_priority_labels(self, priority: Priority) -> List[str]:
425
+ def get_priority_labels(self, priority: Priority) -> list[str]:
426
426
  """Get possible label names for a priority (GitHub-style).
427
427
 
428
428
  Args:
@@ -445,7 +445,7 @@ class PriorityMapper(BaseMapper):
445
445
 
446
446
  return priority_labels.get(priority, [])
447
447
 
448
- def detect_priority_from_labels(self, labels: List[str]) -> Priority:
448
+ def detect_priority_from_labels(self, labels: list[str]) -> Priority:
449
449
  """Detect priority from issue labels (GitHub-style).
450
450
 
451
451
  Args:
@@ -478,12 +478,12 @@ class PriorityMapper(BaseMapper):
478
478
  class MapperRegistry:
479
479
  """Registry for managing mappers across different adapters."""
480
480
 
481
- _state_mappers: Dict[str, StateMapper] = {}
482
- _priority_mappers: Dict[str, PriorityMapper] = {}
481
+ _state_mappers: dict[str, StateMapper] = {}
482
+ _priority_mappers: dict[str, PriorityMapper] = {}
483
483
 
484
484
  @classmethod
485
485
  def get_state_mapper(
486
- self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
486
+ cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
487
487
  ) -> StateMapper:
488
488
  """Get or create state mapper for adapter type.
489
489
 
@@ -496,13 +496,13 @@ class MapperRegistry:
496
496
 
497
497
  """
498
498
  cache_key = f"{adapter_type}_{hash(str(custom_mappings))}"
499
- if cache_key not in self._state_mappers:
500
- self._state_mappers[cache_key] = StateMapper(adapter_type, custom_mappings)
501
- return self._state_mappers[cache_key]
499
+ if cache_key not in cls._state_mappers:
500
+ cls._state_mappers[cache_key] = StateMapper(adapter_type, custom_mappings)
501
+ return cls._state_mappers[cache_key]
502
502
 
503
503
  @classmethod
504
504
  def get_priority_mapper(
505
- self, adapter_type: str, custom_mappings: Optional[Dict[str, Any]] = None
505
+ cls, adapter_type: str, custom_mappings: Optional[dict[str, Any]] = None
506
506
  ) -> PriorityMapper:
507
507
  """Get or create priority mapper for adapter type.
508
508
 
@@ -515,11 +515,11 @@ class MapperRegistry:
515
515
 
516
516
  """
517
517
  cache_key = f"{adapter_type}_{hash(str(custom_mappings))}"
518
- if cache_key not in self._priority_mappers:
519
- self._priority_mappers[cache_key] = PriorityMapper(
518
+ if cache_key not in cls._priority_mappers:
519
+ cls._priority_mappers[cache_key] = PriorityMapper(
520
520
  adapter_type, custom_mappings
521
521
  )
522
- return self._priority_mappers[cache_key]
522
+ return cls._priority_mappers[cache_key]
523
523
 
524
524
  @classmethod
525
525
  def clear_cache(cls) -> None:
@@ -2,7 +2,7 @@
2
2
 
3
3
  from datetime import datetime
4
4
  from enum import Enum
5
- from typing import Any, Dict, List, Optional
5
+ from typing import Any, Optional
6
6
 
7
7
  from pydantic import BaseModel, ConfigDict, Field
8
8
 
@@ -38,7 +38,7 @@ class TicketState(str, Enum):
38
38
  CLOSED = "closed"
39
39
 
40
40
  @classmethod
41
- def valid_transitions(cls) -> Dict[str, List[str]]:
41
+ def valid_transitions(cls) -> dict[str, list[str]]:
42
42
  """Define valid state transitions."""
43
43
  return {
44
44
  cls.OPEN: [cls.IN_PROGRESS, cls.WAITING, cls.BLOCKED, cls.CLOSED],
@@ -66,12 +66,12 @@ class BaseTicket(BaseModel):
66
66
  description: Optional[str] = Field(None, description="Detailed description")
67
67
  state: TicketState = Field(TicketState.OPEN, description="Current state")
68
68
  priority: Priority = Field(Priority.MEDIUM, description="Priority level")
69
- tags: List[str] = Field(default_factory=list, description="Tags/labels")
69
+ tags: list[str] = Field(default_factory=list, description="Tags/labels")
70
70
  created_at: Optional[datetime] = Field(None, description="Creation timestamp")
71
71
  updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
72
72
 
73
73
  # Metadata for field mapping to different systems
74
- metadata: Dict[str, Any] = Field(
74
+ metadata: dict[str, Any] = Field(
75
75
  default_factory=dict, description="System-specific metadata and field mappings"
76
76
  )
77
77
 
@@ -82,11 +82,11 @@ class Epic(BaseTicket):
82
82
  ticket_type: TicketType = Field(
83
83
  default=TicketType.EPIC, frozen=True, description="Always EPIC type"
84
84
  )
85
- child_issues: List[str] = Field(
85
+ child_issues: list[str] = Field(
86
86
  default_factory=list, description="IDs of child issues"
87
87
  )
88
88
 
89
- def validate_hierarchy(self) -> List[str]:
89
+ def validate_hierarchy(self) -> list[str]:
90
90
  """Validate epic hierarchy rules.
91
91
 
92
92
  Returns:
@@ -106,7 +106,7 @@ class Task(BaseTicket):
106
106
  parent_issue: Optional[str] = Field(None, description="Parent issue ID (for tasks)")
107
107
  parent_epic: Optional[str] = Field(None, description="Parent epic ID (for issues)")
108
108
  assignee: Optional[str] = Field(None, description="Assigned user")
109
- children: List[str] = Field(default_factory=list, description="Child task IDs")
109
+ children: list[str] = Field(default_factory=list, description="Child task IDs")
110
110
 
111
111
  # Additional fields common across systems
112
112
  estimated_hours: Optional[float] = Field(None, description="Time estimate")
@@ -124,7 +124,7 @@ class Task(BaseTicket):
124
124
  """Check if this is a sub-task."""
125
125
  return self.ticket_type in (TicketType.TASK, TicketType.SUBTASK)
126
126
 
127
- def validate_hierarchy(self) -> List[str]:
127
+ def validate_hierarchy(self) -> list[str]:
128
128
  """Validate ticket hierarchy rules.
129
129
 
130
130
  Returns:
@@ -160,7 +160,7 @@ class Comment(BaseModel):
160
160
  author: Optional[str] = Field(None, description="Comment author")
161
161
  content: str = Field(..., min_length=1, description="Comment text")
162
162
  created_at: Optional[datetime] = Field(None, description="Creation timestamp")
163
- metadata: Dict[str, Any] = Field(
163
+ metadata: dict[str, Any] = Field(
164
164
  default_factory=dict, description="System-specific metadata"
165
165
  )
166
166
 
@@ -171,7 +171,7 @@ class SearchQuery(BaseModel):
171
171
  query: Optional[str] = Field(None, description="Text search query")
172
172
  state: Optional[TicketState] = Field(None, description="Filter by state")
173
173
  priority: Optional[Priority] = Field(None, description="Filter by priority")
174
- tags: Optional[List[str]] = Field(None, description="Filter by tags")
174
+ tags: Optional[list[str]] = Field(None, description="Filter by tags")
175
175
  assignee: Optional[str] = Field(None, description="Filter by assignee")
176
176
  limit: int = Field(10, gt=0, le=100, description="Maximum results")
177
177
  offset: int = Field(0, ge=0, description="Result offset for pagination")
@@ -14,7 +14,10 @@ import os
14
14
  from dataclasses import asdict, dataclass, field
15
15
  from enum import Enum
16
16
  from pathlib import Path
17
- from typing import Any, Dict, List, Optional
17
+ from typing import Any, Optional, TYPE_CHECKING
18
+
19
+ if TYPE_CHECKING:
20
+ from .env_discovery import DiscoveryResult
18
21
 
19
22
  logger = logging.getLogger(__name__)
20
23
 
@@ -69,9 +72,9 @@ class AdapterConfig:
69
72
  project_id: Optional[str] = None
70
73
 
71
74
  # Additional adapter-specific configuration
72
- additional_config: Dict[str, Any] = field(default_factory=dict)
75
+ additional_config: dict[str, Any] = field(default_factory=dict)
73
76
 
74
- def to_dict(self) -> Dict[str, Any]:
77
+ def to_dict(self) -> dict[str, Any]:
75
78
  """Convert to dictionary, filtering None values."""
76
79
  result = {}
77
80
  for key, value in asdict(self).items():
@@ -80,7 +83,7 @@ class AdapterConfig:
80
83
  return result
81
84
 
82
85
  @classmethod
83
- def from_dict(cls, data: Dict[str, Any]) -> "AdapterConfig":
86
+ def from_dict(cls, data: dict[str, Any]) -> "AdapterConfig":
84
87
  """Create from dictionary."""
85
88
  # Extract known fields
86
89
  known_fields = {
@@ -126,14 +129,14 @@ class ProjectConfig:
126
129
  api_key: Optional[str] = None
127
130
  project_id: Optional[str] = None
128
131
  team_id: Optional[str] = None
129
- additional_config: Dict[str, Any] = field(default_factory=dict)
132
+ additional_config: dict[str, Any] = field(default_factory=dict)
130
133
 
131
- def to_dict(self) -> Dict[str, Any]:
134
+ def to_dict(self) -> dict[str, Any]:
132
135
  """Convert to dictionary."""
133
136
  return {k: v for k, v in asdict(self).items() if v is not None}
134
137
 
135
138
  @classmethod
136
- def from_dict(cls, data: Dict[str, Any]) -> "ProjectConfig":
139
+ def from_dict(cls, data: dict[str, Any]) -> "ProjectConfig":
137
140
  """Create from dictionary."""
138
141
  return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
139
142
 
@@ -143,18 +146,18 @@ class HybridConfig:
143
146
  """Configuration for hybrid mode (multi-adapter sync)."""
144
147
 
145
148
  enabled: bool = False
146
- adapters: List[str] = field(default_factory=list)
149
+ adapters: list[str] = field(default_factory=list)
147
150
  primary_adapter: Optional[str] = None
148
151
  sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
149
152
 
150
- def to_dict(self) -> Dict[str, Any]:
153
+ def to_dict(self) -> dict[str, Any]:
151
154
  """Convert to dictionary."""
152
155
  result = asdict(self)
153
156
  result["sync_strategy"] = self.sync_strategy.value
154
157
  return result
155
158
 
156
159
  @classmethod
157
- def from_dict(cls, data: Dict[str, Any]) -> "HybridConfig":
160
+ def from_dict(cls, data: dict[str, Any]) -> "HybridConfig":
158
161
  """Create from dictionary."""
159
162
  data = data.copy()
160
163
  if "sync_strategy" in data:
@@ -167,11 +170,11 @@ class TicketerConfig:
167
170
  """Complete ticketer configuration with hierarchical resolution."""
168
171
 
169
172
  default_adapter: str = "aitrackdown"
170
- project_configs: Dict[str, ProjectConfig] = field(default_factory=dict)
171
- adapters: Dict[str, AdapterConfig] = field(default_factory=dict)
173
+ project_configs: dict[str, ProjectConfig] = field(default_factory=dict)
174
+ adapters: dict[str, AdapterConfig] = field(default_factory=dict)
172
175
  hybrid_mode: Optional[HybridConfig] = None
173
176
 
174
- def to_dict(self) -> Dict[str, Any]:
177
+ def to_dict(self) -> dict[str, Any]:
175
178
  """Convert to dictionary for JSON serialization."""
176
179
  return {
177
180
  "default_adapter": self.default_adapter,
@@ -185,7 +188,7 @@ class TicketerConfig:
185
188
  }
186
189
 
187
190
  @classmethod
188
- def from_dict(cls, data: Dict[str, Any]) -> "TicketerConfig":
191
+ def from_dict(cls, data: dict[str, Any]) -> "TicketerConfig":
189
192
  """Create from dictionary."""
190
193
  # Parse project configs
191
194
  project_configs = {}
@@ -216,7 +219,7 @@ class ConfigValidator:
216
219
  """Validate adapter configurations."""
217
220
 
218
221
  @staticmethod
219
- def validate_linear_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
222
+ def validate_linear_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
220
223
  """Validate Linear adapter configuration.
221
224
 
222
225
  Returns:
@@ -238,7 +241,7 @@ class ConfigValidator:
238
241
  return True, None
239
242
 
240
243
  @staticmethod
241
- def validate_github_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
244
+ def validate_github_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
242
245
  """Validate GitHub adapter configuration.
243
246
 
244
247
  Returns:
@@ -267,7 +270,7 @@ class ConfigValidator:
267
270
  return True, None
268
271
 
269
272
  @staticmethod
270
- def validate_jira_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
273
+ def validate_jira_config(config: dict[str, Any]) -> tuple[bool, Optional[str]]:
271
274
  """Validate JIRA adapter configuration.
272
275
 
273
276
  Returns:
@@ -288,7 +291,7 @@ class ConfigValidator:
288
291
 
289
292
  @staticmethod
290
293
  def validate_aitrackdown_config(
291
- config: Dict[str, Any],
294
+ config: dict[str, Any],
292
295
  ) -> tuple[bool, Optional[str]]:
293
296
  """Validate AITrackdown adapter configuration.
294
297
 
@@ -302,7 +305,7 @@ class ConfigValidator:
302
305
 
303
306
  @classmethod
304
307
  def validate(
305
- cls, adapter_type: str, config: Dict[str, Any]
308
+ cls, adapter_type: str, config: dict[str, Any]
306
309
  ) -> tuple[bool, Optional[str]]:
307
310
  """Validate configuration for any adapter type.
308
311
 
@@ -459,8 +462,8 @@ class ConfigResolver:
459
462
  def resolve_adapter_config(
460
463
  self,
461
464
  adapter_name: Optional[str] = None,
462
- cli_overrides: Optional[Dict[str, Any]] = None,
463
- ) -> Dict[str, Any]:
465
+ cli_overrides: Optional[dict[str, Any]] = None,
466
+ ) -> dict[str, Any]:
464
467
  """Resolve adapter configuration with hierarchical precedence.
465
468
 
466
469
  Resolution order (highest to lowest priority):
@@ -551,7 +554,7 @@ class ConfigResolver:
551
554
 
552
555
  return resolved_config
553
556
 
554
- def _get_env_overrides(self, adapter_type: str) -> Dict[str, Any]:
557
+ def _get_env_overrides(self, adapter_type: str) -> dict[str, Any]:
555
558
  """Get configuration overrides from environment variables.
556
559
 
557
560
  Args: