mcp-ticketer 0.2.0__py3-none-any.whl → 2.2.9__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.
Files changed (160) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/_version_scm.py +1 -0
  4. mcp_ticketer/adapters/__init__.py +2 -0
  5. mcp_ticketer/adapters/aitrackdown.py +930 -52
  6. mcp_ticketer/adapters/asana/__init__.py +15 -0
  7. mcp_ticketer/adapters/asana/adapter.py +1537 -0
  8. mcp_ticketer/adapters/asana/client.py +292 -0
  9. mcp_ticketer/adapters/asana/mappers.py +348 -0
  10. mcp_ticketer/adapters/asana/types.py +146 -0
  11. mcp_ticketer/adapters/github/__init__.py +26 -0
  12. mcp_ticketer/adapters/github/adapter.py +3229 -0
  13. mcp_ticketer/adapters/github/client.py +335 -0
  14. mcp_ticketer/adapters/github/mappers.py +797 -0
  15. mcp_ticketer/adapters/github/queries.py +692 -0
  16. mcp_ticketer/adapters/github/types.py +460 -0
  17. mcp_ticketer/adapters/hybrid.py +58 -16
  18. mcp_ticketer/adapters/jira/__init__.py +35 -0
  19. mcp_ticketer/adapters/jira/adapter.py +1351 -0
  20. mcp_ticketer/adapters/jira/client.py +271 -0
  21. mcp_ticketer/adapters/jira/mappers.py +246 -0
  22. mcp_ticketer/adapters/jira/queries.py +216 -0
  23. mcp_ticketer/adapters/jira/types.py +304 -0
  24. mcp_ticketer/adapters/linear/__init__.py +1 -1
  25. mcp_ticketer/adapters/linear/adapter.py +3810 -462
  26. mcp_ticketer/adapters/linear/client.py +312 -69
  27. mcp_ticketer/adapters/linear/mappers.py +305 -85
  28. mcp_ticketer/adapters/linear/queries.py +317 -17
  29. mcp_ticketer/adapters/linear/types.py +187 -64
  30. mcp_ticketer/adapters/linear.py +2 -2
  31. mcp_ticketer/analysis/__init__.py +56 -0
  32. mcp_ticketer/analysis/dependency_graph.py +255 -0
  33. mcp_ticketer/analysis/health_assessment.py +304 -0
  34. mcp_ticketer/analysis/orphaned.py +218 -0
  35. mcp_ticketer/analysis/project_status.py +594 -0
  36. mcp_ticketer/analysis/similarity.py +224 -0
  37. mcp_ticketer/analysis/staleness.py +266 -0
  38. mcp_ticketer/automation/__init__.py +11 -0
  39. mcp_ticketer/automation/project_updates.py +378 -0
  40. mcp_ticketer/cache/memory.py +9 -8
  41. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  42. mcp_ticketer/cli/auggie_configure.py +116 -15
  43. mcp_ticketer/cli/codex_configure.py +274 -82
  44. mcp_ticketer/cli/configure.py +1323 -151
  45. mcp_ticketer/cli/cursor_configure.py +314 -0
  46. mcp_ticketer/cli/diagnostics.py +209 -114
  47. mcp_ticketer/cli/discover.py +297 -26
  48. mcp_ticketer/cli/gemini_configure.py +119 -26
  49. mcp_ticketer/cli/init_command.py +880 -0
  50. mcp_ticketer/cli/install_mcp_server.py +418 -0
  51. mcp_ticketer/cli/instruction_commands.py +435 -0
  52. mcp_ticketer/cli/linear_commands.py +256 -130
  53. mcp_ticketer/cli/main.py +140 -1284
  54. mcp_ticketer/cli/mcp_configure.py +1013 -100
  55. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  56. mcp_ticketer/cli/migrate_config.py +12 -8
  57. mcp_ticketer/cli/platform_commands.py +123 -0
  58. mcp_ticketer/cli/platform_detection.py +477 -0
  59. mcp_ticketer/cli/platform_installer.py +545 -0
  60. mcp_ticketer/cli/project_update_commands.py +350 -0
  61. mcp_ticketer/cli/python_detection.py +126 -0
  62. mcp_ticketer/cli/queue_commands.py +15 -15
  63. mcp_ticketer/cli/setup_command.py +794 -0
  64. mcp_ticketer/cli/simple_health.py +84 -59
  65. mcp_ticketer/cli/ticket_commands.py +1375 -0
  66. mcp_ticketer/cli/update_checker.py +313 -0
  67. mcp_ticketer/cli/utils.py +195 -72
  68. mcp_ticketer/core/__init__.py +64 -1
  69. mcp_ticketer/core/adapter.py +618 -18
  70. mcp_ticketer/core/config.py +77 -68
  71. mcp_ticketer/core/env_discovery.py +75 -16
  72. mcp_ticketer/core/env_loader.py +121 -97
  73. mcp_ticketer/core/exceptions.py +32 -24
  74. mcp_ticketer/core/http_client.py +26 -26
  75. mcp_ticketer/core/instructions.py +405 -0
  76. mcp_ticketer/core/label_manager.py +732 -0
  77. mcp_ticketer/core/mappers.py +42 -30
  78. mcp_ticketer/core/milestone_manager.py +252 -0
  79. mcp_ticketer/core/models.py +566 -19
  80. mcp_ticketer/core/onepassword_secrets.py +379 -0
  81. mcp_ticketer/core/priority_matcher.py +463 -0
  82. mcp_ticketer/core/project_config.py +189 -49
  83. mcp_ticketer/core/project_utils.py +281 -0
  84. mcp_ticketer/core/project_validator.py +376 -0
  85. mcp_ticketer/core/registry.py +3 -3
  86. mcp_ticketer/core/session_state.py +176 -0
  87. mcp_ticketer/core/state_matcher.py +592 -0
  88. mcp_ticketer/core/url_parser.py +425 -0
  89. mcp_ticketer/core/validators.py +69 -0
  90. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  91. mcp_ticketer/mcp/__init__.py +29 -1
  92. mcp_ticketer/mcp/__main__.py +60 -0
  93. mcp_ticketer/mcp/server/__init__.py +25 -0
  94. mcp_ticketer/mcp/server/__main__.py +60 -0
  95. mcp_ticketer/mcp/server/constants.py +58 -0
  96. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  97. mcp_ticketer/mcp/server/dto.py +195 -0
  98. mcp_ticketer/mcp/server/main.py +1343 -0
  99. mcp_ticketer/mcp/server/response_builder.py +206 -0
  100. mcp_ticketer/mcp/server/routing.py +723 -0
  101. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  102. mcp_ticketer/mcp/server/tools/__init__.py +69 -0
  103. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  104. mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
  105. mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
  106. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  107. mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
  108. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  109. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
  110. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  111. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  112. mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
  113. mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
  114. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  115. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  116. mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
  117. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  118. mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
  119. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  120. mcp_ticketer/queue/__init__.py +1 -0
  121. mcp_ticketer/queue/health_monitor.py +168 -136
  122. mcp_ticketer/queue/manager.py +78 -63
  123. mcp_ticketer/queue/queue.py +108 -21
  124. mcp_ticketer/queue/run_worker.py +2 -2
  125. mcp_ticketer/queue/ticket_registry.py +213 -155
  126. mcp_ticketer/queue/worker.py +96 -58
  127. mcp_ticketer/utils/__init__.py +5 -0
  128. mcp_ticketer/utils/token_utils.py +246 -0
  129. mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
  130. mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
  131. mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
  132. py_mcp_installer/examples/phase3_demo.py +178 -0
  133. py_mcp_installer/scripts/manage_version.py +54 -0
  134. py_mcp_installer/setup.py +6 -0
  135. py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
  136. py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
  137. py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
  138. py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
  139. py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
  140. py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
  141. py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
  142. py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
  143. py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
  144. py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
  145. py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
  146. py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
  147. py_mcp_installer/src/py_mcp_installer/types.py +222 -0
  148. py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
  149. py_mcp_installer/tests/__init__.py +0 -0
  150. py_mcp_installer/tests/platforms/__init__.py +0 -0
  151. py_mcp_installer/tests/test_platform_detector.py +17 -0
  152. mcp_ticketer/adapters/github.py +0 -1354
  153. mcp_ticketer/adapters/jira.py +0 -1011
  154. mcp_ticketer/mcp/server.py +0 -1895
  155. mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
  156. mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
  157. mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
  158. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
  159. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
  160. {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
@@ -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, cast
10
10
 
11
11
  import yaml
12
12
  from pydantic import BaseModel, Field, field_validator, model_validator
@@ -27,103 +27,109 @@ 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(default=None)
42
+ owner: str | None = Field(default=None)
43
+ repo: str | None = Field(default=None)
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:
54
55
  raise ValueError("GitHub token is required")
55
- return v
56
+ return cast(str, v)
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:
63
65
  raise ValueError("GitHub owner is required")
64
- return v
66
+ return cast(str, v)
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:
72
75
  raise ValueError("GitHub repo is required")
73
- return v
76
+ return cast(str, v)
74
77
 
75
78
 
76
79
  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(default=None)
84
+ email: str | None = Field(default=None)
85
+ api_token: str | None = Field(default=None)
86
+ project_key: str | None = Field(default=None)
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:
93
97
  raise ValueError("JIRA server URL is required")
94
- return v.rstrip("/")
98
+ return cast(str, v).rstrip("/")
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:
102
107
  raise ValueError("JIRA email is required")
103
- return v
108
+ return cast(str, v)
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:
111
117
  raise ValueError("JIRA API token is required")
112
- return v
118
+ return cast(str, v)
113
119
 
114
120
 
115
121
  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(default=None)
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,12 +137,13 @@ 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:
138
145
  raise ValueError("Linear API key is required")
139
- return v
146
+ return cast(str, v)
140
147
 
141
148
 
142
149
  class AITrackdownConfig(BaseAdapterConfig):
@@ -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:
@@ -336,16 +343,16 @@ class ConfigurationManager:
336
343
  try:
337
344
  with open(config_path, encoding="utf-8") as file:
338
345
  if config_path.suffix.lower() in [".yaml", ".yml"]:
339
- return yaml.safe_load(file) or {}
346
+ return cast(dict[str, Any], yaml.safe_load(file) or {})
340
347
  elif config_path.suffix.lower() == ".json":
341
- return json.load(file)
348
+ return cast(dict[str, Any], json.load(file))
342
349
  else:
343
350
  # Try YAML first, then JSON
344
351
  content = file.read()
345
352
  try:
346
- return yaml.safe_load(content) or {}
353
+ return cast(dict[str, Any], yaml.safe_load(content) or {})
347
354
  except yaml.YAMLError:
348
- return json.loads(content)
355
+ return cast(dict[str, Any], json.loads(content))
349
356
  except Exception as e:
350
357
  logger.error(f"Error loading config file {config_path}: {e}")
351
358
  return {}
@@ -366,30 +373,26 @@ class ConfigurationManager:
366
373
  "aitrackdown": {
367
374
  "type": "aitrackdown",
368
375
  "enabled": True,
369
- "base_path": str(Path.home() / ".mcp-ticketer" / ".aitrackdown")
376
+ "base_path": str(
377
+ Path.home() / ".mcp-ticketer" / ".aitrackdown"
378
+ ),
370
379
  }
371
380
  },
372
- "default_adapter": "aitrackdown"
381
+ "default_adapter": "aitrackdown",
373
382
  }
374
383
 
375
384
  # Convert discovered adapters to config format
376
- config_data = {
377
- "adapters": {},
378
- "default_adapter": None
379
- }
385
+ config_data: dict[str, Any] = {"adapters": {}, "default_adapter": None}
380
386
 
381
387
  for adapter in discovered.adapters:
382
- adapter_config = {
383
- "type": adapter.adapter_type,
384
- "enabled": True
385
- }
388
+ adapter_config = {"type": adapter.adapter_type, "enabled": True}
386
389
 
387
390
  # Add adapter-specific configuration from discovered config
388
391
  adapter_config.update(adapter.config)
389
392
 
390
393
  # Ensure type is set correctly (remove 'adapter' key if present)
391
- if 'adapter' in adapter_config:
392
- del adapter_config['adapter']
394
+ if "adapter" in adapter_config:
395
+ del adapter_config["adapter"]
393
396
 
394
397
  # Use adapter type as the key name
395
398
  adapter_name = adapter.adapter_type
@@ -399,20 +402,26 @@ class ConfigurationManager:
399
402
  if config_data["default_adapter"] is None:
400
403
  config_data["default_adapter"] = adapter.adapter_type
401
404
 
402
- logger.info(f"Discovered {len(config_data['adapters'])} adapter(s) from environment")
405
+ logger.info(
406
+ f"Discovered {len(config_data['adapters'])} adapter(s) from environment"
407
+ )
403
408
  return config_data
404
409
 
405
410
  except ImportError:
406
- logger.warning("Environment discovery not available, using aitrackdown fallback")
411
+ logger.warning(
412
+ "Environment discovery not available, using aitrackdown fallback"
413
+ )
407
414
  return {
408
415
  "adapters": {
409
416
  "aitrackdown": {
410
417
  "type": "aitrackdown",
411
418
  "enabled": True,
412
- "base_path": str(Path.home() / ".mcp-ticketer" / ".aitrackdown")
419
+ "base_path": str(
420
+ Path.home() / ".mcp-ticketer" / ".aitrackdown"
421
+ ),
413
422
  }
414
423
  },
415
- "default_adapter": "aitrackdown"
424
+ "default_adapter": "aitrackdown",
416
425
  }
417
426
  except Exception as e:
418
427
  logger.error(f"Environment discovery failed: {e}")
@@ -421,10 +430,12 @@ class ConfigurationManager:
421
430
  "aitrackdown": {
422
431
  "type": "aitrackdown",
423
432
  "enabled": True,
424
- "base_path": str(Path.home() / ".mcp-ticketer" / ".aitrackdown")
433
+ "base_path": str(
434
+ Path.home() / ".mcp-ticketer" / ".aitrackdown"
435
+ ),
425
436
  }
426
437
  },
427
- "default_adapter": "aitrackdown"
438
+ "default_adapter": "aitrackdown",
428
439
  }
429
440
 
430
441
  def get_config(self) -> AppConfig:
@@ -433,7 +444,7 @@ class ConfigurationManager:
433
444
  return self.load_config()
434
445
  return self._config
435
446
 
436
- def get_adapter_config(self, adapter_name: str) -> Optional[BaseAdapterConfig]:
447
+ def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
437
448
  """Get configuration for a specific adapter."""
438
449
  config = self.get_config()
439
450
  return config.get_adapter_config(adapter_name)
@@ -453,9 +464,7 @@ class ConfigurationManager:
453
464
  config = self.get_config()
454
465
  return config.logging
455
466
 
456
- def reload_config(
457
- self, config_file: Optional[Union[str, Path]] = None
458
- ) -> AppConfig:
467
+ def reload_config(self, config_file: str | Path | None = None) -> AppConfig:
459
468
  """Reload configuration from file."""
460
469
  # Clear cache
461
470
  self.load_config.cache_clear()
@@ -488,7 +497,7 @@ class ConfigurationManager:
488
497
  self._config_cache[key] = value
489
498
  return value
490
499
 
491
- def create_sample_config(self, output_path: Union[str, Path]) -> None:
500
+ def create_sample_config(self, output_path: str | Path) -> None:
492
501
  """Create a sample configuration file."""
493
502
  sample_config = {
494
503
  "adapters": {
@@ -535,11 +544,11 @@ def get_config() -> AppConfig:
535
544
  return config_manager.get_config()
536
545
 
537
546
 
538
- def get_adapter_config(adapter_name: str) -> Optional[BaseAdapterConfig]:
547
+ def get_adapter_config(adapter_name: str) -> BaseAdapterConfig | None:
539
548
  """Get configuration for a specific adapter."""
540
549
  return config_manager.get_adapter_config(adapter_name)
541
550
 
542
551
 
543
- def reload_config(config_file: Optional[Union[str, Path]] = None) -> AppConfig:
552
+ def reload_config(config_file: str | Path | None = None) -> AppConfig:
544
553
  """Reload the global configuration."""
545
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.
@@ -229,6 +244,7 @@ class EnvDiscovery:
229
244
 
230
245
  # First, load from actual environment variables (lowest priority)
231
246
  import os
247
+
232
248
  actual_env = {k: v for k, v in os.environ.items() if v}
233
249
  merged_env.update(actual_env)
234
250
  if actual_env:
@@ -240,7 +256,22 @@ class EnvDiscovery:
240
256
  file_path = self.project_path / env_file
241
257
  if file_path.exists():
242
258
  try:
243
- 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
+
244
275
  # Filter out None values
245
276
  env_vars = {k: v for k, v in env_vars.items() if v is not None}
246
277
  merged_env.update(env_vars)
@@ -254,7 +285,7 @@ class EnvDiscovery:
254
285
 
255
286
  def _find_key_value(
256
287
  self, env_vars: dict[str, str], patterns: list[str]
257
- ) -> Optional[str]:
288
+ ) -> str | None:
258
289
  """Find first matching key value from patterns.
259
290
 
260
291
  Args:
@@ -272,7 +303,7 @@ class EnvDiscovery:
272
303
 
273
304
  def _detect_linear(
274
305
  self, env_vars: dict[str, str], found_in: str
275
- ) -> Optional[DiscoveredAdapter]:
306
+ ) -> DiscoveredAdapter | None:
276
307
  """Detect Linear adapter configuration.
277
308
 
278
309
  Args:
@@ -300,7 +331,7 @@ class EnvDiscovery:
300
331
  team_identifier = self._find_key_value(env_vars, LINEAR_TEAM_PATTERNS)
301
332
  if team_identifier:
302
333
  # Determine if it's a team_id (UUID format) or team_key (short string)
303
- if len(team_identifier) > 20 and '-' in team_identifier:
334
+ if len(team_identifier) > 20 and "-" in team_identifier:
304
335
  # Looks like a UUID (team_id)
305
336
  config["team_id"] = team_identifier
306
337
  else:
@@ -326,7 +357,7 @@ class EnvDiscovery:
326
357
 
327
358
  def _detect_github(
328
359
  self, env_vars: dict[str, str], found_in: str
329
- ) -> Optional[DiscoveredAdapter]:
360
+ ) -> DiscoveredAdapter | None:
330
361
  """Detect GitHub adapter configuration.
331
362
 
332
363
  Args:
@@ -385,7 +416,7 @@ class EnvDiscovery:
385
416
 
386
417
  def _detect_jira(
387
418
  self, env_vars: dict[str, str], found_in: str
388
- ) -> Optional[DiscoveredAdapter]:
419
+ ) -> DiscoveredAdapter | None:
389
420
  """Detect JIRA adapter configuration.
390
421
 
391
422
  Args:
@@ -441,7 +472,7 @@ class EnvDiscovery:
441
472
 
442
473
  def _detect_aitrackdown(
443
474
  self, env_vars: dict[str, str], found_in: str
444
- ) -> Optional[DiscoveredAdapter]:
475
+ ) -> DiscoveredAdapter | None:
445
476
  """Detect AITrackdown adapter configuration.
446
477
 
447
478
  Args:
@@ -454,11 +485,32 @@ class EnvDiscovery:
454
485
  """
455
486
  base_path = self._find_key_value(env_vars, AITRACKDOWN_PATH_PATTERNS)
456
487
 
457
- # Also check if .aitrackdown directory exists
488
+ # Check for explicit MCP_TICKETER_ADAPTER setting
489
+ explicit_adapter = env_vars.get("MCP_TICKETER_ADAPTER")
490
+ if explicit_adapter and explicit_adapter != "aitrackdown":
491
+ # If another adapter is explicitly set, don't detect aitrackdown
492
+ return None
493
+
494
+ # Check if .aitrackdown directory exists
458
495
  aitrackdown_dir = self.project_path / ".aitrackdown"
496
+
497
+ # Only detect aitrackdown if:
498
+ # 1. There's an explicit base_path setting, OR
499
+ # 2. There's a .aitrackdown directory AND no other adapter variables are present
500
+ has_other_adapter_vars = (
501
+ any(key.startswith("LINEAR_") for key in env_vars)
502
+ or any(key.startswith("GITHUB_") for key in env_vars)
503
+ or any(key.startswith("JIRA_") for key in env_vars)
504
+ )
505
+
459
506
  if not base_path and not aitrackdown_dir.exists():
460
507
  return None
461
508
 
509
+ if not base_path and has_other_adapter_vars:
510
+ # Don't detect aitrackdown if other adapter variables are present
511
+ # unless explicitly configured
512
+ return None
513
+
462
514
  config: dict[str, Any] = {
463
515
  "adapter": AdapterType.AITRACKDOWN.value,
464
516
  }
@@ -468,8 +520,15 @@ class EnvDiscovery:
468
520
  else:
469
521
  config["base_path"] = ".aitrackdown"
470
522
 
471
- # AITrackdown has no required external credentials
472
- confidence = 1.0 if aitrackdown_dir.exists() else 0.7
523
+ # Lower confidence when other adapter variables are present
524
+ if has_other_adapter_vars:
525
+ confidence = 0.3 # Low confidence when other adapters are configured
526
+ elif base_path:
527
+ confidence = 1.0 # High confidence when explicitly configured
528
+ elif aitrackdown_dir.exists():
529
+ confidence = 0.8 # Medium confidence when directory exists
530
+ else:
531
+ confidence = 0.5 # Low confidence as fallback
473
532
 
474
533
  return DiscoveredAdapter(
475
534
  adapter_type=AdapterType.AITRACKDOWN.value,
@@ -593,8 +652,8 @@ class EnvDiscovery:
593
652
  return warnings
594
653
 
595
654
 
596
- def discover_config(project_path: Optional[Path] = None) -> DiscoveryResult:
597
- """Convenience function to discover configuration.
655
+ def discover_config(project_path: Path | None = None) -> DiscoveryResult:
656
+ """Discover configuration from environment files.
598
657
 
599
658
  Args:
600
659
  project_path: Path to project root (defaults to cwd)