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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -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/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +58 -16
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -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 +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- 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/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1284
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- 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 +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- 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/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -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 +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.2.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.2.0.dist-info/RECORD +0 -58
- mcp_ticketer-0.2.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.2.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +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, 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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
120
|
-
workspace:
|
|
121
|
-
team_key:
|
|
122
|
-
team_id:
|
|
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:
|
|
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:
|
|
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,
|
|
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:
|
|
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) ->
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
392
|
-
del adapter_config[
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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) ->
|
|
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:
|
|
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) ->
|
|
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:
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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__(
|
|
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
|
-
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
) ->
|
|
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
|
-
#
|
|
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
|
-
#
|
|
472
|
-
|
|
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:
|
|
597
|
-
"""
|
|
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)
|