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