mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
mcp_ticketer/core/config.py
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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:
|
|
42
|
-
owner:
|
|
43
|
-
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:
|
|
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:
|
|
81
|
-
email:
|
|
82
|
-
api_token:
|
|
83
|
-
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:
|
|
@@ -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,23 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
116
122
|
"""Linear adapter configuration."""
|
|
117
123
|
|
|
118
124
|
type: AdapterType = AdapterType.LINEAR
|
|
119
|
-
api_key:
|
|
120
|
-
workspace:
|
|
121
|
-
team_key: str
|
|
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
|
|
122
129
|
api_url: str = "https://api.linear.app/graphql"
|
|
123
130
|
|
|
131
|
+
@model_validator(mode="after")
|
|
132
|
+
def validate_team_identifier(self) -> "LinearConfig":
|
|
133
|
+
"""Ensure either team_key or team_id is provided."""
|
|
134
|
+
if not self.team_key and not self.team_id:
|
|
135
|
+
raise ValueError("Either team_key or team_id is required")
|
|
136
|
+
return self
|
|
137
|
+
|
|
124
138
|
@field_validator("api_key", mode="before")
|
|
125
139
|
@classmethod
|
|
126
|
-
def validate_api_key(cls, v):
|
|
140
|
+
def validate_api_key(cls, v: Any) -> str:
|
|
141
|
+
"""Validate Linear API key from config or environment."""
|
|
127
142
|
if not v:
|
|
128
143
|
v = os.getenv("LINEAR_API_KEY")
|
|
129
144
|
if not v:
|
|
@@ -142,7 +157,7 @@ class QueueConfig(BaseModel):
|
|
|
142
157
|
"""Queue configuration."""
|
|
143
158
|
|
|
144
159
|
provider: str = "sqlite"
|
|
145
|
-
connection_string:
|
|
160
|
+
connection_string: str | None = None
|
|
146
161
|
batch_size: int = 10
|
|
147
162
|
max_concurrent: int = 5
|
|
148
163
|
retry_attempts: int = 3
|
|
@@ -154,7 +169,7 @@ class LoggingConfig(BaseModel):
|
|
|
154
169
|
|
|
155
170
|
level: str = "INFO"
|
|
156
171
|
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
157
|
-
file:
|
|
172
|
+
file: str | None = None
|
|
158
173
|
max_size: str = "10MB"
|
|
159
174
|
backup_count: int = 5
|
|
160
175
|
|
|
@@ -163,15 +178,15 @@ class AppConfig(BaseModel):
|
|
|
163
178
|
"""Main application configuration."""
|
|
164
179
|
|
|
165
180
|
adapters: dict[
|
|
166
|
-
str,
|
|
181
|
+
str, GitHubConfig | JiraConfig | LinearConfig | AITrackdownConfig
|
|
167
182
|
] = {}
|
|
168
183
|
queue: QueueConfig = QueueConfig()
|
|
169
184
|
logging: LoggingConfig = LoggingConfig()
|
|
170
185
|
cache_ttl: int = 300 # Cache TTL in seconds
|
|
171
|
-
default_adapter:
|
|
186
|
+
default_adapter: str | None = None
|
|
172
187
|
|
|
173
188
|
@model_validator(mode="after")
|
|
174
|
-
def validate_adapters(self):
|
|
189
|
+
def validate_adapters(self) -> "AppConfig":
|
|
175
190
|
"""Validate adapter configurations."""
|
|
176
191
|
adapters = self.adapters
|
|
177
192
|
|
|
@@ -188,7 +203,7 @@ class AppConfig(BaseModel):
|
|
|
188
203
|
|
|
189
204
|
return self
|
|
190
205
|
|
|
191
|
-
def get_adapter_config(self, adapter_name: str) ->
|
|
206
|
+
def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
|
|
192
207
|
"""Get configuration for a specific adapter."""
|
|
193
208
|
return self.adapters.get(adapter_name)
|
|
194
209
|
|
|
@@ -203,7 +218,7 @@ class ConfigurationManager:
|
|
|
203
218
|
"""Centralized configuration management with caching and validation."""
|
|
204
219
|
|
|
205
220
|
_instance: Optional["ConfigurationManager"] = None
|
|
206
|
-
_config:
|
|
221
|
+
_config: AppConfig | None = None
|
|
207
222
|
_config_file_paths: list[Path] = []
|
|
208
223
|
|
|
209
224
|
def __new__(cls) -> "ConfigurationManager":
|
|
@@ -212,7 +227,7 @@ class ConfigurationManager:
|
|
|
212
227
|
cls._instance = super().__new__(cls)
|
|
213
228
|
return cls._instance
|
|
214
229
|
|
|
215
|
-
def __init__(self):
|
|
230
|
+
def __init__(self) -> None:
|
|
216
231
|
"""Initialize configuration manager."""
|
|
217
232
|
if not hasattr(self, "_initialized"):
|
|
218
233
|
self._initialized = True
|
|
@@ -258,7 +273,7 @@ class ConfigurationManager:
|
|
|
258
273
|
logger.debug("No project-local config files found, will use defaults")
|
|
259
274
|
|
|
260
275
|
@lru_cache(maxsize=1)
|
|
261
|
-
def load_config(self, config_file:
|
|
276
|
+
def load_config(self, config_file: str | Path | None = None) -> AppConfig:
|
|
262
277
|
"""Load and validate configuration from file and environment.
|
|
263
278
|
|
|
264
279
|
Args:
|
|
@@ -283,6 +298,12 @@ class ConfigurationManager:
|
|
|
283
298
|
# Load from first available config file
|
|
284
299
|
config_data = self._load_config_file(self._config_file_paths[0])
|
|
285
300
|
logger.info(f"Loaded configuration from: {self._config_file_paths[0]}")
|
|
301
|
+
else:
|
|
302
|
+
# No config file found - use empty config
|
|
303
|
+
config_data = {"adapters": {}, "default_adapter": None}
|
|
304
|
+
|
|
305
|
+
# Use saved configuration only - no automatic discovery
|
|
306
|
+
# Discovery should be done explicitly via 'mcp-ticketer init' or 'mcp-ticketer discover'
|
|
286
307
|
|
|
287
308
|
# Parse adapter configurations
|
|
288
309
|
if "adapters" in config_data:
|
|
@@ -290,13 +311,21 @@ class ConfigurationManager:
|
|
|
290
311
|
for name, adapter_config in config_data["adapters"].items():
|
|
291
312
|
adapter_type = adapter_config.get("type", "").lower()
|
|
292
313
|
|
|
314
|
+
# If no type specified, try to infer from adapter name
|
|
315
|
+
if not adapter_type:
|
|
316
|
+
adapter_type = name.lower()
|
|
317
|
+
|
|
293
318
|
if adapter_type == "github":
|
|
319
|
+
adapter_config["type"] = "github"
|
|
294
320
|
parsed_adapters[name] = GitHubConfig(**adapter_config)
|
|
295
321
|
elif adapter_type == "jira":
|
|
322
|
+
adapter_config["type"] = "jira"
|
|
296
323
|
parsed_adapters[name] = JiraConfig(**adapter_config)
|
|
297
324
|
elif adapter_type == "linear":
|
|
325
|
+
adapter_config["type"] = "linear"
|
|
298
326
|
parsed_adapters[name] = LinearConfig(**adapter_config)
|
|
299
327
|
elif adapter_type == "aitrackdown":
|
|
328
|
+
adapter_config["type"] = "aitrackdown"
|
|
300
329
|
parsed_adapters[name] = AITrackdownConfig(**adapter_config)
|
|
301
330
|
else:
|
|
302
331
|
logger.warning(
|
|
@@ -328,13 +357,94 @@ class ConfigurationManager:
|
|
|
328
357
|
logger.error(f"Error loading config file {config_path}: {e}")
|
|
329
358
|
return {}
|
|
330
359
|
|
|
360
|
+
def _discover_from_environment(self) -> dict[str, Any]:
|
|
361
|
+
"""Discover configuration from environment variables."""
|
|
362
|
+
try:
|
|
363
|
+
from .env_discovery import EnvDiscovery
|
|
364
|
+
|
|
365
|
+
discovery = EnvDiscovery()
|
|
366
|
+
discovered = discovery.discover()
|
|
367
|
+
|
|
368
|
+
if not discovered.adapters:
|
|
369
|
+
logger.info("No adapters discovered from environment variables")
|
|
370
|
+
# Return minimal config with aitrackdown as fallback
|
|
371
|
+
return {
|
|
372
|
+
"adapters": {
|
|
373
|
+
"aitrackdown": {
|
|
374
|
+
"type": "aitrackdown",
|
|
375
|
+
"enabled": True,
|
|
376
|
+
"base_path": str(
|
|
377
|
+
Path.home() / ".mcp-ticketer" / ".aitrackdown"
|
|
378
|
+
),
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
"default_adapter": "aitrackdown",
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
# Convert discovered adapters to config format
|
|
385
|
+
config_data: dict[str, Any] = {"adapters": {}, "default_adapter": None}
|
|
386
|
+
|
|
387
|
+
for adapter in discovered.adapters:
|
|
388
|
+
adapter_config = {"type": adapter.adapter_type, "enabled": True}
|
|
389
|
+
|
|
390
|
+
# Add adapter-specific configuration from discovered config
|
|
391
|
+
adapter_config.update(adapter.config)
|
|
392
|
+
|
|
393
|
+
# Ensure type is set correctly (remove 'adapter' key if present)
|
|
394
|
+
if "adapter" in adapter_config:
|
|
395
|
+
del adapter_config["adapter"]
|
|
396
|
+
|
|
397
|
+
# Use adapter type as the key name
|
|
398
|
+
adapter_name = adapter.adapter_type
|
|
399
|
+
config_data["adapters"][adapter_name] = adapter_config
|
|
400
|
+
|
|
401
|
+
# Set first discovered adapter as default
|
|
402
|
+
if config_data["default_adapter"] is None:
|
|
403
|
+
config_data["default_adapter"] = adapter.adapter_type
|
|
404
|
+
|
|
405
|
+
logger.info(
|
|
406
|
+
f"Discovered {len(config_data['adapters'])} adapter(s) from environment"
|
|
407
|
+
)
|
|
408
|
+
return config_data
|
|
409
|
+
|
|
410
|
+
except ImportError:
|
|
411
|
+
logger.warning(
|
|
412
|
+
"Environment discovery not available, using aitrackdown fallback"
|
|
413
|
+
)
|
|
414
|
+
return {
|
|
415
|
+
"adapters": {
|
|
416
|
+
"aitrackdown": {
|
|
417
|
+
"type": "aitrackdown",
|
|
418
|
+
"enabled": True,
|
|
419
|
+
"base_path": str(
|
|
420
|
+
Path.home() / ".mcp-ticketer" / ".aitrackdown"
|
|
421
|
+
),
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
"default_adapter": "aitrackdown",
|
|
425
|
+
}
|
|
426
|
+
except Exception as e:
|
|
427
|
+
logger.error(f"Environment discovery failed: {e}")
|
|
428
|
+
return {
|
|
429
|
+
"adapters": {
|
|
430
|
+
"aitrackdown": {
|
|
431
|
+
"type": "aitrackdown",
|
|
432
|
+
"enabled": True,
|
|
433
|
+
"base_path": str(
|
|
434
|
+
Path.home() / ".mcp-ticketer" / ".aitrackdown"
|
|
435
|
+
),
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
"default_adapter": "aitrackdown",
|
|
439
|
+
}
|
|
440
|
+
|
|
331
441
|
def get_config(self) -> AppConfig:
|
|
332
442
|
"""Get the current configuration."""
|
|
333
443
|
if self._config is None:
|
|
334
444
|
return self.load_config()
|
|
335
445
|
return self._config
|
|
336
446
|
|
|
337
|
-
def get_adapter_config(self, adapter_name: str) ->
|
|
447
|
+
def get_adapter_config(self, adapter_name: str) -> BaseAdapterConfig | None:
|
|
338
448
|
"""Get configuration for a specific adapter."""
|
|
339
449
|
config = self.get_config()
|
|
340
450
|
return config.get_adapter_config(adapter_name)
|
|
@@ -354,9 +464,7 @@ class ConfigurationManager:
|
|
|
354
464
|
config = self.get_config()
|
|
355
465
|
return config.logging
|
|
356
466
|
|
|
357
|
-
def reload_config(
|
|
358
|
-
self, config_file: Optional[Union[str, Path]] = None
|
|
359
|
-
) -> AppConfig:
|
|
467
|
+
def reload_config(self, config_file: str | Path | None = None) -> AppConfig:
|
|
360
468
|
"""Reload configuration from file."""
|
|
361
469
|
# Clear cache
|
|
362
470
|
self.load_config.cache_clear()
|
|
@@ -389,7 +497,7 @@ class ConfigurationManager:
|
|
|
389
497
|
self._config_cache[key] = value
|
|
390
498
|
return value
|
|
391
499
|
|
|
392
|
-
def create_sample_config(self, output_path:
|
|
500
|
+
def create_sample_config(self, output_path: str | Path) -> None:
|
|
393
501
|
"""Create a sample configuration file."""
|
|
394
502
|
sample_config = {
|
|
395
503
|
"adapters": {
|
|
@@ -436,11 +544,11 @@ def get_config() -> AppConfig:
|
|
|
436
544
|
return config_manager.get_config()
|
|
437
545
|
|
|
438
546
|
|
|
439
|
-
def get_adapter_config(adapter_name: str) ->
|
|
547
|
+
def get_adapter_config(adapter_name: str) -> BaseAdapterConfig | None:
|
|
440
548
|
"""Get configuration for a specific adapter."""
|
|
441
549
|
return config_manager.get_adapter_config(adapter_name)
|
|
442
550
|
|
|
443
551
|
|
|
444
|
-
def reload_config(config_file:
|
|
552
|
+
def reload_config(config_file: str | Path | None = None) -> AppConfig:
|
|
445
553
|
"""Reload the global configuration."""
|
|
446
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
|
|
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__)
|
|
@@ -30,8 +32,10 @@ LINEAR_KEY_PATTERNS = [
|
|
|
30
32
|
|
|
31
33
|
LINEAR_TEAM_PATTERNS = [
|
|
32
34
|
"LINEAR_TEAM_ID",
|
|
35
|
+
"LINEAR_TEAM_KEY", # Added support for team key (e.g., "BTA")
|
|
33
36
|
"LINEAR_TEAM",
|
|
34
37
|
"MCP_TICKETER_LINEAR_TEAM_ID",
|
|
38
|
+
"MCP_TICKETER_LINEAR_TEAM_KEY",
|
|
35
39
|
]
|
|
36
40
|
|
|
37
41
|
LINEAR_PROJECT_PATTERNS = [
|
|
@@ -123,7 +127,7 @@ class DiscoveryResult:
|
|
|
123
127
|
warnings: list[str] = field(default_factory=list)
|
|
124
128
|
env_files_found: list[str] = field(default_factory=list)
|
|
125
129
|
|
|
126
|
-
def get_primary_adapter(self) ->
|
|
130
|
+
def get_primary_adapter(self) -> DiscoveredAdapter | None:
|
|
127
131
|
"""Get the adapter with highest confidence and completeness."""
|
|
128
132
|
if not self.adapters:
|
|
129
133
|
return None
|
|
@@ -134,7 +138,7 @@ class DiscoveryResult:
|
|
|
134
138
|
)
|
|
135
139
|
return sorted_adapters[0]
|
|
136
140
|
|
|
137
|
-
def get_adapter_by_type(self, adapter_type: str) ->
|
|
141
|
+
def get_adapter_by_type(self, adapter_type: str) -> DiscoveredAdapter | None:
|
|
138
142
|
"""Get discovered adapter by type."""
|
|
139
143
|
for adapter in self.adapters:
|
|
140
144
|
if adapter.adapter_type == adapter_type:
|
|
@@ -153,14 +157,27 @@ class EnvDiscovery:
|
|
|
153
157
|
".env.development",
|
|
154
158
|
]
|
|
155
159
|
|
|
156
|
-
def __init__(
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
project_path: Path | None = None,
|
|
163
|
+
enable_1password: bool = True,
|
|
164
|
+
onepassword_config: OnePasswordConfig | None = None,
|
|
165
|
+
):
|
|
157
166
|
"""Initialize discovery.
|
|
158
167
|
|
|
159
168
|
Args:
|
|
160
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
|
|
161
172
|
|
|
162
173
|
"""
|
|
163
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
|
+
)
|
|
164
181
|
|
|
165
182
|
def discover(self) -> DiscoveryResult:
|
|
166
183
|
"""Discover adapter configurations from environment files.
|
|
@@ -210,7 +227,11 @@ class EnvDiscovery:
|
|
|
210
227
|
return result
|
|
211
228
|
|
|
212
229
|
def _load_env_files(self, result: DiscoveryResult) -> dict[str, str]:
|
|
213
|
-
"""Load environment variables from files.
|
|
230
|
+
"""Load environment variables from files and actual environment.
|
|
231
|
+
|
|
232
|
+
Priority order (highest to lowest):
|
|
233
|
+
1. .env files (highest priority)
|
|
234
|
+
2. Environment variables (lowest priority)
|
|
214
235
|
|
|
215
236
|
Args:
|
|
216
237
|
result: DiscoveryResult to update with found files
|
|
@@ -221,12 +242,36 @@ class EnvDiscovery:
|
|
|
221
242
|
"""
|
|
222
243
|
merged_env: dict[str, str] = {}
|
|
223
244
|
|
|
224
|
-
#
|
|
245
|
+
# First, load from actual environment variables (lowest priority)
|
|
246
|
+
import os
|
|
247
|
+
|
|
248
|
+
actual_env = {k: v for k, v in os.environ.items() if v}
|
|
249
|
+
merged_env.update(actual_env)
|
|
250
|
+
if actual_env:
|
|
251
|
+
result.env_files_found.append("environment")
|
|
252
|
+
logger.debug(f"Loaded {len(actual_env)} variables from environment")
|
|
253
|
+
|
|
254
|
+
# Load files in reverse order (higher priority than environment)
|
|
225
255
|
for env_file in reversed(self.ENV_FILE_ORDER):
|
|
226
256
|
file_path = self.project_path / env_file
|
|
227
257
|
if file_path.exists():
|
|
228
258
|
try:
|
|
229
|
-
|
|
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
|
+
|
|
230
275
|
# Filter out None values
|
|
231
276
|
env_vars = {k: v for k, v in env_vars.items() if v is not None}
|
|
232
277
|
merged_env.update(env_vars)
|
|
@@ -240,7 +285,7 @@ class EnvDiscovery:
|
|
|
240
285
|
|
|
241
286
|
def _find_key_value(
|
|
242
287
|
self, env_vars: dict[str, str], patterns: list[str]
|
|
243
|
-
) ->
|
|
288
|
+
) -> str | None:
|
|
244
289
|
"""Find first matching key value from patterns.
|
|
245
290
|
|
|
246
291
|
Args:
|
|
@@ -258,7 +303,7 @@ class EnvDiscovery:
|
|
|
258
303
|
|
|
259
304
|
def _detect_linear(
|
|
260
305
|
self, env_vars: dict[str, str], found_in: str
|
|
261
|
-
) ->
|
|
306
|
+
) -> DiscoveredAdapter | None:
|
|
262
307
|
"""Detect Linear adapter configuration.
|
|
263
308
|
|
|
264
309
|
Args:
|
|
@@ -282,13 +327,19 @@ class EnvDiscovery:
|
|
|
282
327
|
missing_fields: list[str] = []
|
|
283
328
|
confidence = 0.6 # Has API key
|
|
284
329
|
|
|
285
|
-
# Extract team
|
|
286
|
-
|
|
287
|
-
if
|
|
288
|
-
|
|
330
|
+
# Extract team identifier (either team_id or team_key is required)
|
|
331
|
+
team_identifier = self._find_key_value(env_vars, LINEAR_TEAM_PATTERNS)
|
|
332
|
+
if team_identifier:
|
|
333
|
+
# Determine if it's a team_id (UUID format) or team_key (short string)
|
|
334
|
+
if len(team_identifier) > 20 and "-" in team_identifier:
|
|
335
|
+
# Looks like a UUID (team_id)
|
|
336
|
+
config["team_id"] = team_identifier
|
|
337
|
+
else:
|
|
338
|
+
# Looks like a short key (team_key)
|
|
339
|
+
config["team_key"] = team_identifier
|
|
289
340
|
confidence += 0.3
|
|
290
341
|
else:
|
|
291
|
-
missing_fields.append("team_id (
|
|
342
|
+
missing_fields.append("team_id or team_key (required)")
|
|
292
343
|
|
|
293
344
|
# Extract project ID (optional)
|
|
294
345
|
project_id = self._find_key_value(env_vars, LINEAR_PROJECT_PATTERNS)
|
|
@@ -306,7 +357,7 @@ class EnvDiscovery:
|
|
|
306
357
|
|
|
307
358
|
def _detect_github(
|
|
308
359
|
self, env_vars: dict[str, str], found_in: str
|
|
309
|
-
) ->
|
|
360
|
+
) -> DiscoveredAdapter | None:
|
|
310
361
|
"""Detect GitHub adapter configuration.
|
|
311
362
|
|
|
312
363
|
Args:
|
|
@@ -365,7 +416,7 @@ class EnvDiscovery:
|
|
|
365
416
|
|
|
366
417
|
def _detect_jira(
|
|
367
418
|
self, env_vars: dict[str, str], found_in: str
|
|
368
|
-
) ->
|
|
419
|
+
) -> DiscoveredAdapter | None:
|
|
369
420
|
"""Detect JIRA adapter configuration.
|
|
370
421
|
|
|
371
422
|
Args:
|
|
@@ -421,7 +472,7 @@ class EnvDiscovery:
|
|
|
421
472
|
|
|
422
473
|
def _detect_aitrackdown(
|
|
423
474
|
self, env_vars: dict[str, str], found_in: str
|
|
424
|
-
) ->
|
|
475
|
+
) -> DiscoveredAdapter | None:
|
|
425
476
|
"""Detect AITrackdown adapter configuration.
|
|
426
477
|
|
|
427
478
|
Args:
|
|
@@ -434,11 +485,32 @@ class EnvDiscovery:
|
|
|
434
485
|
"""
|
|
435
486
|
base_path = self._find_key_value(env_vars, AITRACKDOWN_PATH_PATTERNS)
|
|
436
487
|
|
|
437
|
-
#
|
|
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
|
|
438
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
|
+
|
|
439
506
|
if not base_path and not aitrackdown_dir.exists():
|
|
440
507
|
return None
|
|
441
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
|
+
|
|
442
514
|
config: dict[str, Any] = {
|
|
443
515
|
"adapter": AdapterType.AITRACKDOWN.value,
|
|
444
516
|
}
|
|
@@ -448,8 +520,15 @@ class EnvDiscovery:
|
|
|
448
520
|
else:
|
|
449
521
|
config["base_path"] = ".aitrackdown"
|
|
450
522
|
|
|
451
|
-
#
|
|
452
|
-
|
|
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
|
|
453
532
|
|
|
454
533
|
return DiscoveredAdapter(
|
|
455
534
|
adapter_type=AdapterType.AITRACKDOWN.value,
|
|
@@ -573,8 +652,8 @@ class EnvDiscovery:
|
|
|
573
652
|
return warnings
|
|
574
653
|
|
|
575
654
|
|
|
576
|
-
def discover_config(project_path:
|
|
577
|
-
"""
|
|
655
|
+
def discover_config(project_path: Path | None = None) -> DiscoveryResult:
|
|
656
|
+
"""Discover configuration from environment files.
|
|
578
657
|
|
|
579
658
|
Args:
|
|
580
659
|
project_path: Path to project root (defaults to cwd)
|