mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

Files changed (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,16 @@
1
1
  """Base adapter abstract class for ticket systems."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import builtins
4
6
  from abc import ABC, abstractmethod
5
- from typing import Any, Generic, Optional, TypeVar
7
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
8
 
7
9
  from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
8
10
 
11
+ if TYPE_CHECKING:
12
+ from .models import Attachment
13
+
9
14
  # Generic type for tickets
10
15
  T = TypeVar("T", Epic, Task)
11
16
 
@@ -57,7 +62,7 @@ class BaseAdapter(ABC, Generic[T]):
57
62
  pass
58
63
 
59
64
  @abstractmethod
60
- async def read(self, ticket_id: str) -> Optional[T]:
65
+ async def read(self, ticket_id: str) -> T | None:
61
66
  """Read a ticket by ID.
62
67
 
63
68
  Args:
@@ -70,7 +75,7 @@ class BaseAdapter(ABC, Generic[T]):
70
75
  pass
71
76
 
72
77
  @abstractmethod
73
- async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[T]:
78
+ async def update(self, ticket_id: str, updates: dict[str, Any]) -> T | None:
74
79
  """Update a ticket.
75
80
 
76
81
  Args:
@@ -98,7 +103,7 @@ class BaseAdapter(ABC, Generic[T]):
98
103
 
99
104
  @abstractmethod
100
105
  async def list(
101
- self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
106
+ self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
102
107
  ) -> list[T]:
103
108
  """List tickets with pagination and filters.
104
109
 
@@ -129,7 +134,7 @@ class BaseAdapter(ABC, Generic[T]):
129
134
  @abstractmethod
130
135
  async def transition_state(
131
136
  self, ticket_id: str, target_state: TicketState
132
- ) -> Optional[T]:
137
+ ) -> T | None:
133
138
  """Transition ticket to a new state.
134
139
 
135
140
  Args:
@@ -225,8 +230,8 @@ class BaseAdapter(ABC, Generic[T]):
225
230
  # Epic/Issue/Task Hierarchy Methods
226
231
 
227
232
  async def create_epic(
228
- self, title: str, description: Optional[str] = None, **kwargs
229
- ) -> Optional[Epic]:
233
+ self, title: str, description: str | None = None, **kwargs: Any
234
+ ) -> Epic | None:
230
235
  """Create epic (top-level grouping).
231
236
 
232
237
  Args:
@@ -249,7 +254,7 @@ class BaseAdapter(ABC, Generic[T]):
249
254
  return result
250
255
  return None
251
256
 
252
- async def get_epic(self, epic_id: str) -> Optional[Epic]:
257
+ async def get_epic(self, epic_id: str) -> Epic | None:
253
258
  """Get epic by ID.
254
259
 
255
260
  Args:
@@ -265,7 +270,7 @@ class BaseAdapter(ABC, Generic[T]):
265
270
  return result
266
271
  return None
267
272
 
268
- async def list_epics(self, **kwargs) -> builtins.list[Epic]:
273
+ async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
269
274
  """List all epics.
270
275
 
271
276
  Args:
@@ -284,10 +289,10 @@ class BaseAdapter(ABC, Generic[T]):
284
289
  async def create_issue(
285
290
  self,
286
291
  title: str,
287
- description: Optional[str] = None,
288
- epic_id: Optional[str] = None,
289
- **kwargs,
290
- ) -> Optional[Task]:
292
+ description: str | None = None,
293
+ epic_id: str | None = None,
294
+ **kwargs: Any,
295
+ ) -> Task | None:
291
296
  """Create issue, optionally linked to epic.
292
297
 
293
298
  Args:
@@ -325,8 +330,8 @@ class BaseAdapter(ABC, Generic[T]):
325
330
  return [r for r in results if isinstance(r, Task) and r.is_issue()]
326
331
 
327
332
  async def create_task(
328
- self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
329
- ) -> Optional[Task]:
333
+ self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
334
+ ) -> Task | None:
330
335
  """Create task as sub-ticket of parent issue.
331
336
 
332
337
  Args:
@@ -375,6 +380,70 @@ class BaseAdapter(ABC, Generic[T]):
375
380
  results = await self.list(filters=filters)
376
381
  return [r for r in results if isinstance(r, Task) and r.is_task()]
377
382
 
383
+ # Attachment methods
384
+ async def add_attachment(
385
+ self,
386
+ ticket_id: str,
387
+ file_path: str,
388
+ description: str | None = None,
389
+ ) -> Attachment:
390
+ """Attach a file to a ticket.
391
+
392
+ Args:
393
+ ticket_id: Ticket identifier
394
+ file_path: Local file path to upload
395
+ description: Optional attachment description
396
+
397
+ Returns:
398
+ Created Attachment with metadata
399
+
400
+ Raises:
401
+ NotImplementedError: If adapter doesn't support attachments
402
+ FileNotFoundError: If file doesn't exist
403
+ ValueError: If ticket doesn't exist or upload fails
404
+
405
+ """
406
+ raise NotImplementedError(
407
+ f"{self.__class__.__name__} does not support file attachments. "
408
+ "Use comments to reference external files instead."
409
+ )
410
+
411
+ async def get_attachments(self, ticket_id: str) -> list[Attachment]:
412
+ """Get all attachments for a ticket.
413
+
414
+ Args:
415
+ ticket_id: Ticket identifier
416
+
417
+ Returns:
418
+ List of attachments (empty if none or not supported)
419
+
420
+ """
421
+ raise NotImplementedError(
422
+ f"{self.__class__.__name__} does not support file attachments."
423
+ )
424
+
425
+ async def delete_attachment(
426
+ self,
427
+ ticket_id: str,
428
+ attachment_id: str,
429
+ ) -> bool:
430
+ """Delete an attachment (optional implementation).
431
+
432
+ Args:
433
+ ticket_id: Ticket identifier
434
+ attachment_id: Attachment identifier
435
+
436
+ Returns:
437
+ True if deleted, False otherwise
438
+
439
+ Raises:
440
+ NotImplementedError: If adapter doesn't support deletion
441
+
442
+ """
443
+ raise NotImplementedError(
444
+ f"{self.__class__.__name__} does not support attachment deletion."
445
+ )
446
+
378
447
  async def close(self) -> None:
379
448
  """Close adapter and cleanup resources."""
380
449
  pass
@@ -6,7 +6,7 @@ import os
6
6
  from enum import Enum
7
7
  from functools import lru_cache
8
8
  from pathlib import Path
9
- from typing import Any, Optional, Union
9
+ from typing import Any, Optional
10
10
 
11
11
  import yaml
12
12
  from pydantic import BaseModel, Field, field_validator, model_validator
@@ -27,27 +27,28 @@ class BaseAdapterConfig(BaseModel):
27
27
  """Base configuration for all adapters."""
28
28
 
29
29
  type: AdapterType
30
- name: Optional[str] = None
30
+ name: str | None = None
31
31
  enabled: bool = True
32
32
  timeout: float = 30.0
33
33
  max_retries: int = 3
34
- rate_limit: Optional[dict[str, Any]] = None
34
+ rate_limit: dict[str, Any] | None = None
35
35
 
36
36
 
37
37
  class GitHubConfig(BaseAdapterConfig):
38
38
  """GitHub adapter configuration."""
39
39
 
40
40
  type: AdapterType = AdapterType.GITHUB
41
- token: Optional[str] = Field(None, env="GITHUB_TOKEN")
42
- owner: Optional[str] = Field(None, env="GITHUB_OWNER")
43
- repo: Optional[str] = Field(None, env="GITHUB_REPO")
41
+ token: str | None = Field(None, env="GITHUB_TOKEN")
42
+ owner: str | None = Field(None, env="GITHUB_OWNER")
43
+ repo: str | None = Field(None, env="GITHUB_REPO")
44
44
  api_url: str = "https://api.github.com"
45
45
  use_projects_v2: bool = False
46
- custom_priority_scheme: Optional[dict[str, list[str]]] = None
46
+ custom_priority_scheme: dict[str, list[str]] | None = None
47
47
 
48
48
  @field_validator("token", mode="before")
49
49
  @classmethod
50
- def validate_token(cls, v):
50
+ def validate_token(cls, v: Any) -> str:
51
+ """Validate GitHub token from config or environment."""
51
52
  if not v:
52
53
  v = os.getenv("GITHUB_TOKEN")
53
54
  if not v:
@@ -56,7 +57,8 @@ class GitHubConfig(BaseAdapterConfig):
56
57
 
57
58
  @field_validator("owner", mode="before")
58
59
  @classmethod
59
- def validate_owner(cls, v):
60
+ def validate_owner(cls, v: Any) -> str:
61
+ """Validate GitHub repository owner from config or environment."""
60
62
  if not v:
61
63
  v = os.getenv("GITHUB_OWNER")
62
64
  if not v:
@@ -65,7 +67,8 @@ class GitHubConfig(BaseAdapterConfig):
65
67
 
66
68
  @field_validator("repo", mode="before")
67
69
  @classmethod
68
- def validate_repo(cls, v):
70
+ def validate_repo(cls, v: Any) -> str:
71
+ """Validate GitHub repository name from config or environment."""
69
72
  if not v:
70
73
  v = os.getenv("GITHUB_REPO")
71
74
  if not v:
@@ -77,16 +80,17 @@ class JiraConfig(BaseAdapterConfig):
77
80
  """JIRA adapter configuration."""
78
81
 
79
82
  type: AdapterType = AdapterType.JIRA
80
- server: Optional[str] = Field(None, env="JIRA_SERVER")
81
- email: Optional[str] = Field(None, env="JIRA_EMAIL")
82
- api_token: Optional[str] = Field(None, env="JIRA_API_TOKEN")
83
- project_key: Optional[str] = Field(None, env="JIRA_PROJECT_KEY")
83
+ server: str | None = Field(None, env="JIRA_SERVER")
84
+ email: str | None = Field(None, env="JIRA_EMAIL")
85
+ api_token: str | None = Field(None, env="JIRA_API_TOKEN")
86
+ project_key: str | None = Field(None, env="JIRA_PROJECT_KEY")
84
87
  cloud: bool = True
85
88
  verify_ssl: bool = True
86
89
 
87
90
  @field_validator("server", mode="before")
88
91
  @classmethod
89
- def validate_server(cls, v):
92
+ def validate_server(cls, v: Any) -> str:
93
+ """Validate JIRA server URL from config or environment."""
90
94
  if not v:
91
95
  v = os.getenv("JIRA_SERVER")
92
96
  if not v:
@@ -95,7 +99,8 @@ class JiraConfig(BaseAdapterConfig):
95
99
 
96
100
  @field_validator("email", mode="before")
97
101
  @classmethod
98
- def validate_email(cls, v):
102
+ def validate_email(cls, v: Any) -> str:
103
+ """Validate JIRA user email from config or environment."""
99
104
  if not v:
100
105
  v = os.getenv("JIRA_EMAIL")
101
106
  if not v:
@@ -104,7 +109,8 @@ class JiraConfig(BaseAdapterConfig):
104
109
 
105
110
  @field_validator("api_token", mode="before")
106
111
  @classmethod
107
- def validate_api_token(cls, v):
112
+ def validate_api_token(cls, v: Any) -> str:
113
+ """Validate JIRA API token from config or environment."""
108
114
  if not v:
109
115
  v = os.getenv("JIRA_API_TOKEN")
110
116
  if not v:
@@ -116,14 +122,14 @@ class LinearConfig(BaseAdapterConfig):
116
122
  """Linear adapter configuration."""
117
123
 
118
124
  type: AdapterType = AdapterType.LINEAR
119
- api_key: Optional[str] = Field(None, env="LINEAR_API_KEY")
120
- workspace: Optional[str] = None
121
- team_key: Optional[str] = None # Short team key like "BTA"
122
- team_id: Optional[str] = None # UUID team identifier
125
+ api_key: str | None = Field(None, env="LINEAR_API_KEY")
126
+ workspace: str | None = None
127
+ team_key: str | None = None # Short team key like "BTA"
128
+ team_id: str | None = None # UUID team identifier
123
129
  api_url: str = "https://api.linear.app/graphql"
124
130
 
125
131
  @model_validator(mode="after")
126
- def validate_team_identifier(self):
132
+ def validate_team_identifier(self) -> "LinearConfig":
127
133
  """Ensure either team_key or team_id is provided."""
128
134
  if not self.team_key and not self.team_id:
129
135
  raise ValueError("Either team_key or team_id is required")
@@ -131,7 +137,8 @@ class LinearConfig(BaseAdapterConfig):
131
137
 
132
138
  @field_validator("api_key", mode="before")
133
139
  @classmethod
134
- def validate_api_key(cls, v):
140
+ def validate_api_key(cls, v: Any) -> str:
141
+ """Validate Linear API key from config or environment."""
135
142
  if not v:
136
143
  v = os.getenv("LINEAR_API_KEY")
137
144
  if not v:
@@ -150,7 +157,7 @@ class QueueConfig(BaseModel):
150
157
  """Queue configuration."""
151
158
 
152
159
  provider: str = "sqlite"
153
- connection_string: Optional[str] = None
160
+ connection_string: str | None = None
154
161
  batch_size: int = 10
155
162
  max_concurrent: int = 5
156
163
  retry_attempts: int = 3
@@ -162,7 +169,7 @@ class LoggingConfig(BaseModel):
162
169
 
163
170
  level: str = "INFO"
164
171
  format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
165
- file: Optional[str] = None
172
+ file: str | None = None
166
173
  max_size: str = "10MB"
167
174
  backup_count: int = 5
168
175
 
@@ -171,15 +178,15 @@ class AppConfig(BaseModel):
171
178
  """Main application configuration."""
172
179
 
173
180
  adapters: dict[
174
- str, Union[GitHubConfig, JiraConfig, LinearConfig, AITrackdownConfig]
181
+ str, GitHubConfig | JiraConfig | LinearConfig | AITrackdownConfig
175
182
  ] = {}
176
183
  queue: QueueConfig = QueueConfig()
177
184
  logging: LoggingConfig = LoggingConfig()
178
185
  cache_ttl: int = 300 # Cache TTL in seconds
179
- default_adapter: Optional[str] = None
186
+ default_adapter: str | None = None
180
187
 
181
188
  @model_validator(mode="after")
182
- def validate_adapters(self):
189
+ def validate_adapters(self) -> "AppConfig":
183
190
  """Validate adapter configurations."""
184
191
  adapters = self.adapters
185
192
 
@@ -196,7 +203,7 @@ class AppConfig(BaseModel):
196
203
 
197
204
  return self
198
205
 
199
- def get_adapter_config(self, adapter_name: str) -> Optional[BaseAdapterConfig]:
206
+ def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
200
207
  """Get configuration for a specific adapter."""
201
208
  return self.adapters.get(adapter_name)
202
209
 
@@ -211,7 +218,7 @@ class ConfigurationManager:
211
218
  """Centralized configuration management with caching and validation."""
212
219
 
213
220
  _instance: Optional["ConfigurationManager"] = None
214
- _config: Optional[AppConfig] = None
221
+ _config: AppConfig | None = None
215
222
  _config_file_paths: list[Path] = []
216
223
 
217
224
  def __new__(cls) -> "ConfigurationManager":
@@ -220,7 +227,7 @@ class ConfigurationManager:
220
227
  cls._instance = super().__new__(cls)
221
228
  return cls._instance
222
229
 
223
- def __init__(self):
230
+ def __init__(self) -> None:
224
231
  """Initialize configuration manager."""
225
232
  if not hasattr(self, "_initialized"):
226
233
  self._initialized = True
@@ -266,7 +273,7 @@ class ConfigurationManager:
266
273
  logger.debug("No project-local config files found, will use defaults")
267
274
 
268
275
  @lru_cache(maxsize=1)
269
- def load_config(self, config_file: Optional[Union[str, Path]] = None) -> AppConfig:
276
+ def load_config(self, config_file: str | Path | None = None) -> AppConfig:
270
277
  """Load and validate configuration from file and environment.
271
278
 
272
279
  Args:
@@ -437,7 +444,7 @@ class ConfigurationManager:
437
444
  return self.load_config()
438
445
  return self._config
439
446
 
440
- def get_adapter_config(self, adapter_name: str) -> Optional[BaseAdapterConfig]:
447
+ def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
441
448
  """Get configuration for a specific adapter."""
442
449
  config = self.get_config()
443
450
  return config.get_adapter_config(adapter_name)
@@ -457,9 +464,7 @@ class ConfigurationManager:
457
464
  config = self.get_config()
458
465
  return config.logging
459
466
 
460
- def reload_config(
461
- self, config_file: Optional[Union[str, Path]] = None
462
- ) -> AppConfig:
467
+ def reload_config(self, config_file: str | Path | None = None) -> AppConfig:
463
468
  """Reload configuration from file."""
464
469
  # Clear cache
465
470
  self.load_config.cache_clear()
@@ -492,7 +497,7 @@ class ConfigurationManager:
492
497
  self._config_cache[key] = value
493
498
  return value
494
499
 
495
- def create_sample_config(self, output_path: Union[str, Path]) -> None:
500
+ def create_sample_config(self, output_path: str | Path) -> None:
496
501
  """Create a sample configuration file."""
497
502
  sample_config = {
498
503
  "adapters": {
@@ -539,11 +544,11 @@ def get_config() -> AppConfig:
539
544
  return config_manager.get_config()
540
545
 
541
546
 
542
- def get_adapter_config(adapter_name: str) -> Optional[BaseAdapterConfig]:
547
+ def get_adapter_config(adapter_name: str) -> BaseAdapterConfig | None:
543
548
  """Get configuration for a specific adapter."""
544
549
  return config_manager.get_adapter_config(adapter_name)
545
550
 
546
551
 
547
- def reload_config(config_file: Optional[Union[str, Path]] = None) -> AppConfig:
552
+ def reload_config(config_file: str | Path | None = None) -> AppConfig:
548
553
  """Reload the global configuration."""
549
554
  return config_manager.reload_config(config_file)
@@ -6,15 +6,17 @@ environment files, including:
6
6
  - Support for multiple naming conventions
7
7
  - Project information extraction
8
8
  - Security validation
9
+ - 1Password CLI integration for secret references
9
10
  """
10
11
 
11
12
  import logging
12
13
  from dataclasses import dataclass, field
13
14
  from pathlib import Path
14
- from typing import Any, Optional
15
+ from typing import Any
15
16
 
16
17
  from dotenv import dotenv_values
17
18
 
19
+ from .onepassword_secrets import OnePasswordConfig, OnePasswordSecretsLoader
18
20
  from .project_config import AdapterType
19
21
 
20
22
  logger = logging.getLogger(__name__)
@@ -125,7 +127,7 @@ class DiscoveryResult:
125
127
  warnings: list[str] = field(default_factory=list)
126
128
  env_files_found: list[str] = field(default_factory=list)
127
129
 
128
- def get_primary_adapter(self) -> Optional[DiscoveredAdapter]:
130
+ def get_primary_adapter(self) -> DiscoveredAdapter | None:
129
131
  """Get the adapter with highest confidence and completeness."""
130
132
  if not self.adapters:
131
133
  return None
@@ -136,7 +138,7 @@ class DiscoveryResult:
136
138
  )
137
139
  return sorted_adapters[0]
138
140
 
139
- def get_adapter_by_type(self, adapter_type: str) -> Optional[DiscoveredAdapter]:
141
+ def get_adapter_by_type(self, adapter_type: str) -> DiscoveredAdapter | None:
140
142
  """Get discovered adapter by type."""
141
143
  for adapter in self.adapters:
142
144
  if adapter.adapter_type == adapter_type:
@@ -155,14 +157,27 @@ class EnvDiscovery:
155
157
  ".env.development",
156
158
  ]
157
159
 
158
- def __init__(self, project_path: Optional[Path] = None):
160
+ def __init__(
161
+ self,
162
+ project_path: Path | None = None,
163
+ enable_1password: bool = True,
164
+ onepassword_config: OnePasswordConfig | None = None,
165
+ ):
159
166
  """Initialize discovery.
160
167
 
161
168
  Args:
162
169
  project_path: Path to project root (defaults to cwd)
170
+ enable_1password: Enable 1Password CLI integration for secret resolution
171
+ onepassword_config: Configuration for 1Password integration
163
172
 
164
173
  """
165
174
  self.project_path = project_path or Path.cwd()
175
+ self.enable_1password = enable_1password
176
+ self.op_loader = (
177
+ OnePasswordSecretsLoader(onepassword_config or OnePasswordConfig())
178
+ if enable_1password
179
+ else None
180
+ )
166
181
 
167
182
  def discover(self) -> DiscoveryResult:
168
183
  """Discover adapter configurations from environment files.
@@ -241,7 +256,22 @@ class EnvDiscovery:
241
256
  file_path = self.project_path / env_file
242
257
  if file_path.exists():
243
258
  try:
244
- env_vars = dotenv_values(file_path)
259
+ # Check if file contains 1Password references and use op loader if available
260
+ if self.op_loader and self.enable_1password:
261
+ content = file_path.read_text(encoding="utf-8")
262
+ if "op://" in content:
263
+ logger.info(
264
+ f"Detected 1Password references in {env_file}, "
265
+ "attempting to resolve..."
266
+ )
267
+ env_vars = self.op_loader.load_secrets_from_env_file(
268
+ file_path
269
+ )
270
+ else:
271
+ env_vars = dotenv_values(file_path)
272
+ else:
273
+ env_vars = dotenv_values(file_path)
274
+
245
275
  # Filter out None values
246
276
  env_vars = {k: v for k, v in env_vars.items() if v is not None}
247
277
  merged_env.update(env_vars)
@@ -255,7 +285,7 @@ class EnvDiscovery:
255
285
 
256
286
  def _find_key_value(
257
287
  self, env_vars: dict[str, str], patterns: list[str]
258
- ) -> Optional[str]:
288
+ ) -> str | None:
259
289
  """Find first matching key value from patterns.
260
290
 
261
291
  Args:
@@ -273,7 +303,7 @@ class EnvDiscovery:
273
303
 
274
304
  def _detect_linear(
275
305
  self, env_vars: dict[str, str], found_in: str
276
- ) -> Optional[DiscoveredAdapter]:
306
+ ) -> DiscoveredAdapter | None:
277
307
  """Detect Linear adapter configuration.
278
308
 
279
309
  Args:
@@ -327,7 +357,7 @@ class EnvDiscovery:
327
357
 
328
358
  def _detect_github(
329
359
  self, env_vars: dict[str, str], found_in: str
330
- ) -> Optional[DiscoveredAdapter]:
360
+ ) -> DiscoveredAdapter | None:
331
361
  """Detect GitHub adapter configuration.
332
362
 
333
363
  Args:
@@ -386,7 +416,7 @@ class EnvDiscovery:
386
416
 
387
417
  def _detect_jira(
388
418
  self, env_vars: dict[str, str], found_in: str
389
- ) -> Optional[DiscoveredAdapter]:
419
+ ) -> DiscoveredAdapter | None:
390
420
  """Detect JIRA adapter configuration.
391
421
 
392
422
  Args:
@@ -442,7 +472,7 @@ class EnvDiscovery:
442
472
 
443
473
  def _detect_aitrackdown(
444
474
  self, env_vars: dict[str, str], found_in: str
445
- ) -> Optional[DiscoveredAdapter]:
475
+ ) -> DiscoveredAdapter | None:
446
476
  """Detect AITrackdown adapter configuration.
447
477
 
448
478
  Args:
@@ -622,8 +652,8 @@ class EnvDiscovery:
622
652
  return warnings
623
653
 
624
654
 
625
- def discover_config(project_path: Optional[Path] = None) -> DiscoveryResult:
626
- """Convenience function to discover configuration.
655
+ def discover_config(project_path: Path | None = None) -> DiscoveryResult:
656
+ """Discover configuration from environment files.
627
657
 
628
658
  Args:
629
659
  project_path: Path to project root (defaults to cwd)
@@ -12,7 +12,7 @@ import logging
12
12
  import os
13
13
  from dataclasses import dataclass
14
14
  from pathlib import Path
15
- from typing import Any, Optional
15
+ from typing import Any
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -25,12 +25,13 @@ class EnvKeyConfig:
25
25
  aliases: list[str]
26
26
  description: str
27
27
  required: bool = False
28
- default: Optional[str] = None
28
+ default: str | None = None
29
29
 
30
30
 
31
31
  class UnifiedEnvLoader:
32
- """Unified environment loader that handles multiple naming conventions
33
- and provides consistent environment loading across all contexts.
32
+ """Unified environment loader that handles multiple naming conventions.
33
+
34
+ Provides consistent environment loading across all contexts.
34
35
  """
35
36
 
36
37
  # Define key aliases for all adapters
@@ -105,7 +106,7 @@ class UnifiedEnvLoader:
105
106
  ),
106
107
  }
107
108
 
108
- def __init__(self, project_root: Optional[Path] = None):
109
+ def __init__(self, project_root: Path | None = None):
109
110
  """Initialize the environment loader.
110
111
 
111
112
  Args:
@@ -131,7 +132,7 @@ class UnifiedEnvLoader:
131
132
  # Fallback to current directory
132
133
  return Path.cwd()
133
134
 
134
- def _load_env_files(self):
135
+ def _load_env_files(self) -> None:
135
136
  """Load environment variables from .env files."""
136
137
  env_files = [
137
138
  self.project_root / ".env.local",
@@ -144,7 +145,7 @@ class UnifiedEnvLoader:
144
145
  logger.debug(f"Loading environment from: {env_file}")
145
146
  self._load_env_file(env_file)
146
147
 
147
- def _load_env_file(self, env_file: Path):
148
+ def _load_env_file(self, env_file: Path) -> None:
148
149
  """Load variables from a single .env file."""
149
150
  try:
150
151
  with open(env_file) as f:
@@ -177,8 +178,8 @@ class UnifiedEnvLoader:
177
178
  logger.warning(f"Failed to load {env_file}: {e}")
178
179
 
179
180
  def get_value(
180
- self, config_key: str, config: Optional[dict[str, Any]] = None
181
- ) -> Optional[str]:
181
+ self, config_key: str, config: dict[str, Any] | None = None
182
+ ) -> str | None:
182
183
  """Get a configuration value using the key alias system.
183
184
 
184
185
  Args:
@@ -230,7 +231,7 @@ class UnifiedEnvLoader:
230
231
  return None
231
232
 
232
233
  def get_adapter_config(
233
- self, adapter_name: str, base_config: Optional[dict[str, Any]] = None
234
+ self, adapter_name: str, base_config: dict[str, Any] | None = None
234
235
  ) -> dict[str, Any]:
235
236
  """Get complete configuration for an adapter with environment variable resolution.
236
237
 
@@ -307,7 +308,7 @@ class UnifiedEnvLoader:
307
308
 
308
309
 
309
310
  # Global instance
310
- _env_loader: Optional[UnifiedEnvLoader] = None
311
+ _env_loader: UnifiedEnvLoader | None = None
311
312
 
312
313
 
313
314
  def get_env_loader() -> UnifiedEnvLoader:
@@ -319,9 +320,9 @@ def get_env_loader() -> UnifiedEnvLoader:
319
320
 
320
321
 
321
322
  def load_adapter_config(
322
- adapter_name: str, base_config: Optional[dict[str, Any]] = None
323
+ adapter_name: str, base_config: dict[str, Any] | None = None
323
324
  ) -> dict[str, Any]:
324
- """Convenience function to load adapter configuration with environment variables.
325
+ """Load adapter configuration with environment variables.
325
326
 
326
327
  Args:
327
328
  adapter_name: Name of the adapter ('linear', 'jira', 'github')
@@ -335,7 +336,7 @@ def load_adapter_config(
335
336
 
336
337
 
337
338
  def validate_adapter_config(adapter_name: str, config: dict[str, Any]) -> list[str]:
338
- """Convenience function to validate adapter configuration.
339
+ """Validate adapter configuration.
339
340
 
340
341
  Args:
341
342
  adapter_name: Name of the adapter