mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/cache/__init__.py
CHANGED
mcp_ticketer/cache/memory.py
CHANGED
|
@@ -5,7 +5,7 @@ import hashlib
|
|
|
5
5
|
import json
|
|
6
6
|
import time
|
|
7
7
|
from functools import wraps
|
|
8
|
-
from typing import Any,
|
|
8
|
+
from typing import Any, Callable, Optional
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class CacheEntry:
|
|
@@ -17,6 +17,7 @@ class CacheEntry:
|
|
|
17
17
|
Args:
|
|
18
18
|
value: Cached value
|
|
19
19
|
ttl: Time to live in seconds
|
|
20
|
+
|
|
20
21
|
"""
|
|
21
22
|
self.value = value
|
|
22
23
|
self.expires_at = time.time() + ttl if ttl > 0 else float("inf")
|
|
@@ -34,8 +35,9 @@ class MemoryCache:
|
|
|
34
35
|
|
|
35
36
|
Args:
|
|
36
37
|
default_ttl: Default TTL in seconds (5 minutes)
|
|
38
|
+
|
|
37
39
|
"""
|
|
38
|
-
self._cache:
|
|
40
|
+
self._cache: dict[str, CacheEntry] = {}
|
|
39
41
|
self._default_ttl = default_ttl
|
|
40
42
|
self._lock = asyncio.Lock()
|
|
41
43
|
|
|
@@ -47,6 +49,7 @@ class MemoryCache:
|
|
|
47
49
|
|
|
48
50
|
Returns:
|
|
49
51
|
Cached value or None if not found/expired
|
|
52
|
+
|
|
50
53
|
"""
|
|
51
54
|
async with self._lock:
|
|
52
55
|
entry = self._cache.get(key)
|
|
@@ -57,18 +60,14 @@ class MemoryCache:
|
|
|
57
60
|
del self._cache[key]
|
|
58
61
|
return None
|
|
59
62
|
|
|
60
|
-
async def set(
|
|
61
|
-
self,
|
|
62
|
-
key: str,
|
|
63
|
-
value: Any,
|
|
64
|
-
ttl: Optional[float] = None
|
|
65
|
-
) -> None:
|
|
63
|
+
async def set(self, key: str, value: Any, ttl: Optional[float] = None) -> None:
|
|
66
64
|
"""Set value in cache.
|
|
67
65
|
|
|
68
66
|
Args:
|
|
69
67
|
key: Cache key
|
|
70
68
|
value: Value to cache
|
|
71
69
|
ttl: Optional TTL override
|
|
70
|
+
|
|
72
71
|
"""
|
|
73
72
|
async with self._lock:
|
|
74
73
|
ttl = ttl if ttl is not None else self._default_ttl
|
|
@@ -82,6 +81,7 @@ class MemoryCache:
|
|
|
82
81
|
|
|
83
82
|
Returns:
|
|
84
83
|
True if key was deleted
|
|
84
|
+
|
|
85
85
|
"""
|
|
86
86
|
async with self._lock:
|
|
87
87
|
if key in self._cache:
|
|
@@ -99,11 +99,11 @@ class MemoryCache:
|
|
|
99
99
|
|
|
100
100
|
Returns:
|
|
101
101
|
Number of entries removed
|
|
102
|
+
|
|
102
103
|
"""
|
|
103
104
|
async with self._lock:
|
|
104
105
|
expired_keys = [
|
|
105
|
-
key for key, entry in self._cache.items()
|
|
106
|
-
if entry.is_expired()
|
|
106
|
+
key for key, entry in self._cache.items() if entry.is_expired()
|
|
107
107
|
]
|
|
108
108
|
for key in expired_keys:
|
|
109
109
|
del self._cache[key]
|
|
@@ -123,12 +123,10 @@ class MemoryCache:
|
|
|
123
123
|
|
|
124
124
|
Returns:
|
|
125
125
|
Hash-based cache key
|
|
126
|
+
|
|
126
127
|
"""
|
|
127
128
|
# Create string representation of arguments
|
|
128
|
-
key_data = {
|
|
129
|
-
"args": args,
|
|
130
|
-
"kwargs": sorted(kwargs.items())
|
|
131
|
-
}
|
|
129
|
+
key_data = {"args": args, "kwargs": sorted(kwargs.items())}
|
|
132
130
|
key_str = json.dumps(key_data, sort_keys=True, default=str)
|
|
133
131
|
|
|
134
132
|
# Generate hash
|
|
@@ -138,7 +136,7 @@ class MemoryCache:
|
|
|
138
136
|
def cache_decorator(
|
|
139
137
|
ttl: Optional[float] = None,
|
|
140
138
|
key_prefix: str = "",
|
|
141
|
-
cache_instance: Optional[MemoryCache] = None
|
|
139
|
+
cache_instance: Optional[MemoryCache] = None,
|
|
142
140
|
) -> Callable:
|
|
143
141
|
"""Decorator for caching async function results.
|
|
144
142
|
|
|
@@ -149,6 +147,7 @@ def cache_decorator(
|
|
|
149
147
|
|
|
150
148
|
Returns:
|
|
151
149
|
Decorated function
|
|
150
|
+
|
|
152
151
|
"""
|
|
153
152
|
# Use shared cache instance or create new
|
|
154
153
|
cache = cache_instance or MemoryCache()
|
|
@@ -190,4 +189,4 @@ _global_cache = MemoryCache()
|
|
|
190
189
|
|
|
191
190
|
def get_global_cache() -> MemoryCache:
|
|
192
191
|
"""Get global cache instance."""
|
|
193
|
-
return _global_cache
|
|
192
|
+
return _global_cache
|
mcp_ticketer/cli/__init__.py
CHANGED
mcp_ticketer/cli/configure.py
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
"""Interactive configuration wizard for MCP Ticketer."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
import
|
|
5
|
-
from typing import Optional, Dict, Any
|
|
6
|
-
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
7
5
|
|
|
8
6
|
import typer
|
|
9
7
|
from rich.console import Console
|
|
10
|
-
from rich.prompt import Prompt, Confirm
|
|
11
8
|
from rich.panel import Panel
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
12
10
|
from rich.table import Table
|
|
13
11
|
|
|
14
12
|
from ..core.project_config import (
|
|
15
|
-
ConfigResolver,
|
|
16
|
-
TicketerConfig,
|
|
17
13
|
AdapterConfig,
|
|
18
|
-
ProjectConfig,
|
|
19
|
-
HybridConfig,
|
|
20
14
|
AdapterType,
|
|
15
|
+
ConfigResolver,
|
|
16
|
+
ConfigValidator,
|
|
17
|
+
HybridConfig,
|
|
21
18
|
SyncStrategy,
|
|
22
|
-
|
|
19
|
+
TicketerConfig,
|
|
23
20
|
)
|
|
24
21
|
|
|
25
22
|
console = Console()
|
|
@@ -27,22 +24,20 @@ console = Console()
|
|
|
27
24
|
|
|
28
25
|
def configure_wizard() -> None:
|
|
29
26
|
"""Run interactive configuration wizard."""
|
|
30
|
-
console.print(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
console.print(
|
|
28
|
+
Panel.fit(
|
|
29
|
+
"[bold cyan]MCP-Ticketer Configuration Wizard[/bold cyan]\n"
|
|
30
|
+
"Configure your ticketing system integration",
|
|
31
|
+
border_style="cyan",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
35
34
|
|
|
36
35
|
# Step 1: Choose integration mode
|
|
37
36
|
console.print("\n[bold]Step 1: Integration Mode[/bold]")
|
|
38
37
|
console.print("1. Single Adapter (recommended for most projects)")
|
|
39
38
|
console.print("2. Hybrid Mode (sync across multiple platforms)")
|
|
40
39
|
|
|
41
|
-
mode = Prompt.ask(
|
|
42
|
-
"Select mode",
|
|
43
|
-
choices=["1", "2"],
|
|
44
|
-
default="1"
|
|
45
|
-
)
|
|
40
|
+
mode = Prompt.ask("Select mode", choices=["1", "2"], default="1")
|
|
46
41
|
|
|
47
42
|
if mode == "1":
|
|
48
43
|
config = _configure_single_adapter()
|
|
@@ -54,18 +49,16 @@ def configure_wizard() -> None:
|
|
|
54
49
|
console.print("1. Global (all projects): ~/.mcp-ticketer/config.json")
|
|
55
50
|
console.print("2. Project-specific: .mcp-ticketer/config.json in project root")
|
|
56
51
|
|
|
57
|
-
scope = Prompt.ask(
|
|
58
|
-
"Save configuration as",
|
|
59
|
-
choices=["1", "2"],
|
|
60
|
-
default="2"
|
|
61
|
-
)
|
|
52
|
+
scope = Prompt.ask("Save configuration as", choices=["1", "2"], default="2")
|
|
62
53
|
|
|
63
54
|
resolver = ConfigResolver()
|
|
64
55
|
|
|
65
56
|
if scope == "1":
|
|
66
57
|
# Save global
|
|
67
58
|
resolver.save_global_config(config)
|
|
68
|
-
console.print(
|
|
59
|
+
console.print(
|
|
60
|
+
f"\n[green]✓[/green] Configuration saved globally to {resolver.GLOBAL_CONFIG_PATH}"
|
|
61
|
+
)
|
|
69
62
|
else:
|
|
70
63
|
# Save project-specific
|
|
71
64
|
resolver.save_project_config(config)
|
|
@@ -74,9 +67,11 @@ def configure_wizard() -> None:
|
|
|
74
67
|
|
|
75
68
|
# Show usage instructions
|
|
76
69
|
console.print("\n[bold]Usage:[/bold]")
|
|
77
|
-
console.print(
|
|
70
|
+
console.print(' CLI: [cyan]mcp-ticketer create "Task title"[/cyan]')
|
|
78
71
|
console.print(" MCP: Configure Claude Desktop to use this adapter")
|
|
79
|
-
console.print(
|
|
72
|
+
console.print(
|
|
73
|
+
"\nRun [cyan]mcp-ticketer configure --show[/cyan] to view your configuration"
|
|
74
|
+
)
|
|
80
75
|
|
|
81
76
|
|
|
82
77
|
def _configure_single_adapter() -> TicketerConfig:
|
|
@@ -88,9 +83,7 @@ def _configure_single_adapter() -> TicketerConfig:
|
|
|
88
83
|
console.print("4. Internal/AITrackdown (File-based, no API)")
|
|
89
84
|
|
|
90
85
|
adapter_choice = Prompt.ask(
|
|
91
|
-
"Select system",
|
|
92
|
-
choices=["1", "2", "3", "4"],
|
|
93
|
-
default="1"
|
|
86
|
+
"Select system", choices=["1", "2", "3", "4"], default="1"
|
|
94
87
|
)
|
|
95
88
|
|
|
96
89
|
adapter_type_map = {
|
|
@@ -115,7 +108,7 @@ def _configure_single_adapter() -> TicketerConfig:
|
|
|
115
108
|
# Create config
|
|
116
109
|
config = TicketerConfig(
|
|
117
110
|
default_adapter=adapter_type.value,
|
|
118
|
-
adapters={adapter_type.value: adapter_config}
|
|
111
|
+
adapters={adapter_type.value: adapter_config},
|
|
119
112
|
)
|
|
120
113
|
|
|
121
114
|
return config
|
|
@@ -128,34 +121,22 @@ def _configure_linear() -> AdapterConfig:
|
|
|
128
121
|
# API Key
|
|
129
122
|
api_key = os.getenv("LINEAR_API_KEY") or ""
|
|
130
123
|
if api_key:
|
|
131
|
-
console.print(
|
|
124
|
+
console.print("[dim]Found LINEAR_API_KEY in environment[/dim]")
|
|
132
125
|
use_env = Confirm.ask("Use this API key?", default=True)
|
|
133
126
|
if not use_env:
|
|
134
127
|
api_key = ""
|
|
135
128
|
|
|
136
129
|
if not api_key:
|
|
137
|
-
api_key = Prompt.ask(
|
|
138
|
-
"Linear API Key",
|
|
139
|
-
password=True
|
|
140
|
-
)
|
|
130
|
+
api_key = Prompt.ask("Linear API Key", password=True)
|
|
141
131
|
|
|
142
132
|
# Team ID
|
|
143
|
-
team_id = Prompt.ask(
|
|
144
|
-
"Team ID (optional, e.g., team-abc)",
|
|
145
|
-
default=""
|
|
146
|
-
)
|
|
133
|
+
team_id = Prompt.ask("Team ID (optional, e.g., team-abc)", default="")
|
|
147
134
|
|
|
148
135
|
# Team Key
|
|
149
|
-
team_key = Prompt.ask(
|
|
150
|
-
"Team Key (optional, e.g., ENG)",
|
|
151
|
-
default=""
|
|
152
|
-
)
|
|
136
|
+
team_key = Prompt.ask("Team Key (optional, e.g., ENG)", default="")
|
|
153
137
|
|
|
154
138
|
# Project ID
|
|
155
|
-
project_id = Prompt.ask(
|
|
156
|
-
"Project ID (optional)",
|
|
157
|
-
default=""
|
|
158
|
-
)
|
|
139
|
+
project_id = Prompt.ask("Project ID (optional)", default="")
|
|
159
140
|
|
|
160
141
|
config_dict = {
|
|
161
142
|
"adapter": AdapterType.LINEAR.value,
|
|
@@ -185,9 +166,7 @@ def _configure_jira() -> AdapterConfig:
|
|
|
185
166
|
# Server URL
|
|
186
167
|
server = os.getenv("JIRA_SERVER") or ""
|
|
187
168
|
if not server:
|
|
188
|
-
server = Prompt.ask(
|
|
189
|
-
"JIRA Server URL (e.g., https://company.atlassian.net)"
|
|
190
|
-
)
|
|
169
|
+
server = Prompt.ask("JIRA Server URL (e.g., https://company.atlassian.net)")
|
|
191
170
|
|
|
192
171
|
# Email
|
|
193
172
|
email = os.getenv("JIRA_EMAIL") or ""
|
|
@@ -197,21 +176,17 @@ def _configure_jira() -> AdapterConfig:
|
|
|
197
176
|
# API Token
|
|
198
177
|
api_token = os.getenv("JIRA_API_TOKEN") or ""
|
|
199
178
|
if not api_token:
|
|
200
|
-
console.print(
|
|
201
|
-
|
|
202
|
-
"JIRA API Token",
|
|
203
|
-
password=True
|
|
179
|
+
console.print(
|
|
180
|
+
"[dim]Generate token at: https://id.atlassian.com/manage/api-tokens[/dim]"
|
|
204
181
|
)
|
|
182
|
+
api_token = Prompt.ask("JIRA API Token", password=True)
|
|
205
183
|
|
|
206
184
|
# Project Key
|
|
207
|
-
project_key = Prompt.ask(
|
|
208
|
-
"Default Project Key (optional, e.g., PROJ)",
|
|
209
|
-
default=""
|
|
210
|
-
)
|
|
185
|
+
project_key = Prompt.ask("Default Project Key (optional, e.g., PROJ)", default="")
|
|
211
186
|
|
|
212
187
|
config_dict = {
|
|
213
188
|
"adapter": AdapterType.JIRA.value,
|
|
214
|
-
"server": server.rstrip(
|
|
189
|
+
"server": server.rstrip("/"),
|
|
215
190
|
"email": email,
|
|
216
191
|
"api_token": api_token,
|
|
217
192
|
}
|
|
@@ -235,18 +210,19 @@ def _configure_github() -> AdapterConfig:
|
|
|
235
210
|
# Token
|
|
236
211
|
token = os.getenv("GITHUB_TOKEN") or ""
|
|
237
212
|
if token:
|
|
238
|
-
console.print(
|
|
213
|
+
console.print("[dim]Found GITHUB_TOKEN in environment[/dim]")
|
|
239
214
|
use_env = Confirm.ask("Use this token?", default=True)
|
|
240
215
|
if not use_env:
|
|
241
216
|
token = ""
|
|
242
217
|
|
|
243
218
|
if not token:
|
|
244
|
-
console.print(
|
|
245
|
-
|
|
246
|
-
token = Prompt.ask(
|
|
247
|
-
"GitHub Personal Access Token",
|
|
248
|
-
password=True
|
|
219
|
+
console.print(
|
|
220
|
+
"[dim]Create token at: https://github.com/settings/tokens/new[/dim]"
|
|
249
221
|
)
|
|
222
|
+
console.print(
|
|
223
|
+
"[dim]Required scopes: repo (or public_repo for public repos)[/dim]"
|
|
224
|
+
)
|
|
225
|
+
token = Prompt.ask("GitHub Personal Access Token", password=True)
|
|
250
226
|
|
|
251
227
|
# Repository Owner
|
|
252
228
|
owner = os.getenv("GITHUB_OWNER") or ""
|
|
@@ -279,10 +255,7 @@ def _configure_aitrackdown() -> AdapterConfig:
|
|
|
279
255
|
"""Configure AITrackdown adapter."""
|
|
280
256
|
console.print("\n[bold]Configure AITrackdown (File-based):[/bold]")
|
|
281
257
|
|
|
282
|
-
base_path = Prompt.ask(
|
|
283
|
-
"Base path for ticket storage",
|
|
284
|
-
default=".aitrackdown"
|
|
285
|
-
)
|
|
258
|
+
base_path = Prompt.ask("Base path for ticket storage", default=".aitrackdown")
|
|
286
259
|
|
|
287
260
|
config_dict = {
|
|
288
261
|
"adapter": AdapterType.AITRACKDOWN.value,
|
|
@@ -305,8 +278,7 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
305
278
|
console.print("4. AITrackdown")
|
|
306
279
|
|
|
307
280
|
selections = Prompt.ask(
|
|
308
|
-
"Select adapters (e.g., 1,3 for Linear and GitHub)",
|
|
309
|
-
default="1,3"
|
|
281
|
+
"Select adapters (e.g., 1,3 for Linear and GitHub)", default="1,3"
|
|
310
282
|
)
|
|
311
283
|
|
|
312
284
|
adapter_choices = [s.strip() for s in selections.split(",")]
|
|
@@ -318,7 +290,9 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
318
290
|
"4": AdapterType.AITRACKDOWN,
|
|
319
291
|
}
|
|
320
292
|
|
|
321
|
-
selected_adapters = [
|
|
293
|
+
selected_adapters = [
|
|
294
|
+
adapter_type_map[c] for c in adapter_choices if c in adapter_type_map
|
|
295
|
+
]
|
|
322
296
|
|
|
323
297
|
if len(selected_adapters) < 2:
|
|
324
298
|
console.print("[red]Hybrid mode requires at least 2 adapters[/red]")
|
|
@@ -345,11 +319,13 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
345
319
|
for idx, adapter_type in enumerate(selected_adapters, 1):
|
|
346
320
|
console.print(f"{idx}. {adapter_type.value}")
|
|
347
321
|
|
|
348
|
-
primary_idx = int(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
322
|
+
primary_idx = int(
|
|
323
|
+
Prompt.ask(
|
|
324
|
+
"Primary adapter",
|
|
325
|
+
choices=[str(i) for i in range(1, len(selected_adapters) + 1)],
|
|
326
|
+
default="1",
|
|
327
|
+
)
|
|
328
|
+
)
|
|
353
329
|
|
|
354
330
|
primary_adapter = selected_adapters[primary_idx - 1].value
|
|
355
331
|
|
|
@@ -359,11 +335,7 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
359
335
|
console.print("2. Bidirectional (two-way sync)")
|
|
360
336
|
console.print("3. Mirror (clone tickets across all)")
|
|
361
337
|
|
|
362
|
-
strategy_choice = Prompt.ask(
|
|
363
|
-
"Sync strategy",
|
|
364
|
-
choices=["1", "2", "3"],
|
|
365
|
-
default="1"
|
|
366
|
-
)
|
|
338
|
+
strategy_choice = Prompt.ask("Sync strategy", choices=["1", "2", "3"], default="1")
|
|
367
339
|
|
|
368
340
|
strategy_map = {
|
|
369
341
|
"1": SyncStrategy.PRIMARY_SOURCE,
|
|
@@ -378,14 +350,12 @@ def _configure_hybrid_mode() -> TicketerConfig:
|
|
|
378
350
|
enabled=True,
|
|
379
351
|
adapters=[a.value for a in selected_adapters],
|
|
380
352
|
primary_adapter=primary_adapter,
|
|
381
|
-
sync_strategy=sync_strategy
|
|
353
|
+
sync_strategy=sync_strategy,
|
|
382
354
|
)
|
|
383
355
|
|
|
384
356
|
# Create full config
|
|
385
357
|
config = TicketerConfig(
|
|
386
|
-
default_adapter=primary_adapter,
|
|
387
|
-
adapters=adapters,
|
|
388
|
-
hybrid_mode=hybrid_config
|
|
358
|
+
default_adapter=primary_adapter, adapters=adapters, hybrid_mode=hybrid_config
|
|
389
359
|
)
|
|
390
360
|
|
|
391
361
|
return config
|
|
@@ -440,9 +410,15 @@ def show_current_config() -> None:
|
|
|
440
410
|
|
|
441
411
|
if project_config.hybrid_mode and project_config.hybrid_mode.enabled:
|
|
442
412
|
console.print("\n[bold]Hybrid Mode:[/bold] Enabled")
|
|
443
|
-
console.print(
|
|
444
|
-
|
|
445
|
-
|
|
413
|
+
console.print(
|
|
414
|
+
f" Adapters: {', '.join(project_config.hybrid_mode.adapters)}"
|
|
415
|
+
)
|
|
416
|
+
console.print(
|
|
417
|
+
f" Primary: {project_config.hybrid_mode.primary_adapter}"
|
|
418
|
+
)
|
|
419
|
+
console.print(
|
|
420
|
+
f" Strategy: {project_config.hybrid_mode.sync_strategy.value}"
|
|
421
|
+
)
|
|
446
422
|
else:
|
|
447
423
|
console.print("[yellow]No project-specific configuration found[/yellow]")
|
|
448
424
|
|
|
@@ -469,7 +445,7 @@ def set_adapter_config(
|
|
|
469
445
|
project_id: Optional[str] = None,
|
|
470
446
|
team_id: Optional[str] = None,
|
|
471
447
|
global_scope: bool = False,
|
|
472
|
-
**kwargs
|
|
448
|
+
**kwargs,
|
|
473
449
|
) -> None:
|
|
474
450
|
"""Set specific adapter configuration values.
|
|
475
451
|
|
|
@@ -480,6 +456,7 @@ def set_adapter_config(
|
|
|
480
456
|
team_id: Team ID (Linear)
|
|
481
457
|
global_scope: Save to global config instead of project
|
|
482
458
|
**kwargs: Additional adapter-specific options
|
|
459
|
+
|
|
483
460
|
"""
|
|
484
461
|
resolver = ConfigResolver()
|
|
485
462
|
|
|
@@ -511,8 +488,7 @@ def set_adapter_config(
|
|
|
511
488
|
# Get or create adapter config
|
|
512
489
|
if target_adapter not in config.adapters:
|
|
513
490
|
config.adapters[target_adapter] = AdapterConfig(
|
|
514
|
-
adapter=target_adapter,
|
|
515
|
-
**updates
|
|
491
|
+
adapter=target_adapter, **updates
|
|
516
492
|
)
|
|
517
493
|
else:
|
|
518
494
|
# Update existing
|
mcp_ticketer/cli/discover.py
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
"""CLI command for auto-discovering configuration from .env files."""
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from typing import Optional
|
|
6
5
|
|
|
7
6
|
import typer
|
|
8
7
|
from rich.console import Console
|
|
9
|
-
from rich.table import Table
|
|
10
|
-
from rich.panel import Panel
|
|
11
|
-
from rich import print as rprint
|
|
12
8
|
|
|
13
|
-
from ..core.env_discovery import
|
|
9
|
+
from ..core.env_discovery import DiscoveredAdapter, EnvDiscovery
|
|
14
10
|
from ..core.project_config import (
|
|
15
|
-
ConfigResolver,
|
|
16
|
-
TicketerConfig,
|
|
17
11
|
AdapterConfig,
|
|
12
|
+
ConfigResolver,
|
|
18
13
|
ConfigValidator,
|
|
14
|
+
TicketerConfig,
|
|
19
15
|
)
|
|
20
16
|
|
|
21
17
|
console = Console()
|
|
@@ -31,6 +27,7 @@ def _mask_sensitive(value: str, key: str) -> str:
|
|
|
31
27
|
|
|
32
28
|
Returns:
|
|
33
29
|
Masked or original value
|
|
30
|
+
|
|
34
31
|
"""
|
|
35
32
|
sensitive_keys = ["token", "key", "password", "secret", "api_token"]
|
|
36
33
|
|
|
@@ -52,12 +49,15 @@ def _mask_sensitive(value: str, key: str) -> str:
|
|
|
52
49
|
return value
|
|
53
50
|
|
|
54
51
|
|
|
55
|
-
def _display_discovered_adapter(
|
|
52
|
+
def _display_discovered_adapter(
|
|
53
|
+
adapter: DiscoveredAdapter, discovery: EnvDiscovery
|
|
54
|
+
) -> None:
|
|
56
55
|
"""Display information about a discovered adapter.
|
|
57
56
|
|
|
58
57
|
Args:
|
|
59
58
|
adapter: Discovered adapter to display
|
|
60
59
|
discovery: EnvDiscovery instance for validation
|
|
60
|
+
|
|
61
61
|
"""
|
|
62
62
|
# Header
|
|
63
63
|
completeness = "✅ Complete" if adapter.is_complete() else "⚠️ Incomplete"
|
|
@@ -80,7 +80,9 @@ def _display_discovered_adapter(adapter: DiscoveredAdapter, discovery: EnvDiscov
|
|
|
80
80
|
|
|
81
81
|
# Missing fields
|
|
82
82
|
if adapter.missing_fields:
|
|
83
|
-
console.print(
|
|
83
|
+
console.print(
|
|
84
|
+
f" [yellow]Missing:[/yellow] {', '.join(adapter.missing_fields)}"
|
|
85
|
+
)
|
|
84
86
|
|
|
85
87
|
# Validation warnings
|
|
86
88
|
warnings = discovery.validate_discovered_config(adapter)
|
|
@@ -95,7 +97,7 @@ def show(
|
|
|
95
97
|
None,
|
|
96
98
|
"--path",
|
|
97
99
|
"-p",
|
|
98
|
-
help="Project path to scan (defaults to current directory)"
|
|
100
|
+
help="Project path to scan (defaults to current directory)",
|
|
99
101
|
),
|
|
100
102
|
) -> None:
|
|
101
103
|
"""Show discovered configuration without saving."""
|
|
@@ -119,7 +121,9 @@ def show(
|
|
|
119
121
|
# Show discovered adapters
|
|
120
122
|
if result.adapters:
|
|
121
123
|
console.print("\n[bold]Detected adapter configurations:[/bold]")
|
|
122
|
-
for adapter in sorted(
|
|
124
|
+
for adapter in sorted(
|
|
125
|
+
result.adapters, key=lambda a: a.confidence, reverse=True
|
|
126
|
+
):
|
|
123
127
|
_display_discovered_adapter(adapter, discovery)
|
|
124
128
|
|
|
125
129
|
# Show recommended adapter
|
|
@@ -131,7 +135,9 @@ def show(
|
|
|
131
135
|
)
|
|
132
136
|
else:
|
|
133
137
|
console.print("\n[yellow]No adapter configurations detected[/yellow]")
|
|
134
|
-
console.print(
|
|
138
|
+
console.print(
|
|
139
|
+
"[dim]Make sure your .env file contains adapter credentials[/dim]"
|
|
140
|
+
)
|
|
135
141
|
|
|
136
142
|
# Show warnings
|
|
137
143
|
if result.warnings:
|
|
@@ -143,27 +149,19 @@ def show(
|
|
|
143
149
|
@app.command()
|
|
144
150
|
def save(
|
|
145
151
|
adapter: Optional[str] = typer.Option(
|
|
146
|
-
None,
|
|
147
|
-
"--adapter",
|
|
148
|
-
"-a",
|
|
149
|
-
help="Which adapter to save (defaults to recommended)"
|
|
152
|
+
None, "--adapter", "-a", help="Which adapter to save (defaults to recommended)"
|
|
150
153
|
),
|
|
151
154
|
global_config: bool = typer.Option(
|
|
152
|
-
False,
|
|
153
|
-
"--global",
|
|
154
|
-
"-g",
|
|
155
|
-
help="Save to global config instead of project config"
|
|
155
|
+
False, "--global", "-g", help="Save to global config instead of project config"
|
|
156
156
|
),
|
|
157
157
|
dry_run: bool = typer.Option(
|
|
158
|
-
False,
|
|
159
|
-
"--dry-run",
|
|
160
|
-
help="Show what would be saved without saving"
|
|
158
|
+
False, "--dry-run", help="Show what would be saved without saving"
|
|
161
159
|
),
|
|
162
160
|
project_path: Optional[Path] = typer.Option(
|
|
163
161
|
None,
|
|
164
162
|
"--path",
|
|
165
163
|
"-p",
|
|
166
|
-
help="Project path to scan (defaults to current directory)"
|
|
164
|
+
help="Project path to scan (defaults to current directory)",
|
|
167
165
|
),
|
|
168
166
|
) -> None:
|
|
169
167
|
"""Discover configuration and save to config file.
|
|
@@ -181,7 +179,9 @@ def save(
|
|
|
181
179
|
|
|
182
180
|
if not result.adapters:
|
|
183
181
|
console.print("[red]No adapter configurations detected[/red]")
|
|
184
|
-
console.print(
|
|
182
|
+
console.print(
|
|
183
|
+
"[dim]Make sure your .env file contains adapter credentials[/dim]"
|
|
184
|
+
)
|
|
185
185
|
raise typer.Exit(1)
|
|
186
186
|
|
|
187
187
|
# Determine which adapter to save
|
|
@@ -189,7 +189,9 @@ def save(
|
|
|
189
189
|
discovered_adapter = result.get_adapter_by_type(adapter)
|
|
190
190
|
if not discovered_adapter:
|
|
191
191
|
console.print(f"[red]No configuration found for adapter: {adapter}[/red]")
|
|
192
|
-
console.print(
|
|
192
|
+
console.print(
|
|
193
|
+
f"[dim]Available: {', '.join(a.adapter_type for a in result.adapters)}[/dim]"
|
|
194
|
+
)
|
|
193
195
|
raise typer.Exit(1)
|
|
194
196
|
else:
|
|
195
197
|
# Use recommended adapter
|
|
@@ -207,13 +209,14 @@ def save(
|
|
|
207
209
|
|
|
208
210
|
# Validate configuration
|
|
209
211
|
is_valid, error_msg = ConfigValidator.validate(
|
|
210
|
-
discovered_adapter.adapter_type,
|
|
211
|
-
discovered_adapter.config
|
|
212
|
+
discovered_adapter.adapter_type, discovered_adapter.config
|
|
212
213
|
)
|
|
213
214
|
|
|
214
215
|
if not is_valid:
|
|
215
216
|
console.print(f"\n[red]Configuration validation failed:[/red] {error_msg}")
|
|
216
|
-
console.print(
|
|
217
|
+
console.print(
|
|
218
|
+
"[dim]Fix the configuration in your .env file and try again[/dim]"
|
|
219
|
+
)
|
|
217
220
|
raise typer.Exit(1)
|
|
218
221
|
|
|
219
222
|
if dry_run:
|
|
@@ -247,7 +250,9 @@ def save(
|
|
|
247
250
|
config_location = proj_path / resolver.PROJECT_CONFIG_SUBPATH
|
|
248
251
|
|
|
249
252
|
console.print(f"\n[green]✅ Configuration saved to:[/green] {config_location}")
|
|
250
|
-
console.print(
|
|
253
|
+
console.print(
|
|
254
|
+
f"[green]✅ Default adapter set to:[/green] {discovered_adapter.adapter_type}"
|
|
255
|
+
)
|
|
251
256
|
|
|
252
257
|
except Exception as e:
|
|
253
258
|
console.print(f"\n[red]Failed to save configuration:[/red] {e}")
|
|
@@ -260,7 +265,7 @@ def interactive(
|
|
|
260
265
|
None,
|
|
261
266
|
"--path",
|
|
262
267
|
"-p",
|
|
263
|
-
help="Project path to scan (defaults to current directory)"
|
|
268
|
+
help="Project path to scan (defaults to current directory)",
|
|
264
269
|
),
|
|
265
270
|
) -> None:
|
|
266
271
|
"""Interactive mode for discovering and saving configuration."""
|
|
@@ -284,7 +289,9 @@ def interactive(
|
|
|
284
289
|
# Show discovered adapters
|
|
285
290
|
if not result.adapters:
|
|
286
291
|
console.print("\n[red]No adapter configurations detected[/red]")
|
|
287
|
-
console.print(
|
|
292
|
+
console.print(
|
|
293
|
+
"[dim]Make sure your .env file contains adapter credentials[/dim]"
|
|
294
|
+
)
|
|
288
295
|
raise typer.Exit(1)
|
|
289
296
|
|
|
290
297
|
console.print("\n[bold]Detected adapter configurations:[/bold]")
|
|
@@ -344,7 +351,9 @@ def interactive(
|
|
|
344
351
|
raise typer.Exit(1)
|
|
345
352
|
else: # choice == 4
|
|
346
353
|
adapters_to_save = result.adapters
|
|
347
|
-
default_adapter =
|
|
354
|
+
default_adapter = (
|
|
355
|
+
primary.adapter_type if primary else result.adapters[0].adapter_type
|
|
356
|
+
)
|
|
348
357
|
|
|
349
358
|
# Determine save location
|
|
350
359
|
save_global = choice == 2
|
|
@@ -364,8 +373,7 @@ def interactive(
|
|
|
364
373
|
for discovered_adapter in adapters_to_save:
|
|
365
374
|
# Validate
|
|
366
375
|
is_valid, error_msg = ConfigValidator.validate(
|
|
367
|
-
discovered_adapter.adapter_type,
|
|
368
|
-
discovered_adapter.config
|
|
376
|
+
discovered_adapter.adapter_type, discovered_adapter.config
|
|
369
377
|
)
|
|
370
378
|
|
|
371
379
|
if not is_valid:
|