mcp-ticketer 0.1.33__py3-none-any.whl → 0.1.36__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 +1 -1
- mcp_ticketer/cli/diagnostics.py +66 -121
- mcp_ticketer/cli/main.py +107 -7
- mcp_ticketer/cli/utils.py +36 -2
- mcp_ticketer/core/config.py +37 -31
- mcp_ticketer/core/env_discovery.py +27 -7
- {mcp_ticketer-0.1.33.dist-info → mcp_ticketer-0.1.36.dist-info}/METADATA +2 -1
- {mcp_ticketer-0.1.33.dist-info → mcp_ticketer-0.1.36.dist-info}/RECORD +12 -12
- {mcp_ticketer-0.1.33.dist-info → mcp_ticketer-0.1.36.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.33.dist-info → mcp_ticketer-0.1.36.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.33.dist-info → mcp_ticketer-0.1.36.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.33.dist-info → mcp_ticketer-0.1.36.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
mcp_ticketer/cli/diagnostics.py
CHANGED
|
@@ -15,53 +15,11 @@ from rich.panel import Panel
|
|
|
15
15
|
from rich.table import Table
|
|
16
16
|
from rich.text import Text
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Test if the real config system works
|
|
24
|
-
try:
|
|
25
|
-
config = real_get_config()
|
|
26
|
-
# If we get here, the real config system is working
|
|
27
|
-
return real_get_config
|
|
28
|
-
except Exception:
|
|
29
|
-
# Real config system failed, use fallback
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
except ImportError:
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
# Create a minimal config fallback
|
|
36
|
-
class MockConfig:
|
|
37
|
-
def get_enabled_adapters(self):
|
|
38
|
-
# Try to detect adapters from environment even in fallback
|
|
39
|
-
import os
|
|
40
|
-
adapters = {}
|
|
41
|
-
|
|
42
|
-
# Check for environment variables
|
|
43
|
-
if os.getenv("LINEAR_API_KEY"):
|
|
44
|
-
adapters["linear"] = {"type": "linear", "enabled": True}
|
|
45
|
-
if os.getenv("GITHUB_TOKEN"):
|
|
46
|
-
adapters["github"] = {"type": "github", "enabled": True}
|
|
47
|
-
if os.getenv("JIRA_SERVER"):
|
|
48
|
-
adapters["jira"] = {"type": "jira", "enabled": True}
|
|
49
|
-
|
|
50
|
-
# Always include aitrackdown as fallback
|
|
51
|
-
if not adapters:
|
|
52
|
-
adapters["aitrackdown"] = {"type": "aitrackdown", "enabled": True}
|
|
53
|
-
|
|
54
|
-
return adapters
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def default_adapter(self):
|
|
58
|
-
adapters = self.get_enabled_adapters()
|
|
59
|
-
return list(adapters.keys())[0] if adapters else "aitrackdown"
|
|
60
|
-
|
|
61
|
-
def get_config():
|
|
62
|
-
return MockConfig()
|
|
63
|
-
|
|
64
|
-
return get_config
|
|
18
|
+
def get_config():
|
|
19
|
+
"""Get configuration using the real configuration system."""
|
|
20
|
+
from ..core.config import ConfigurationManager
|
|
21
|
+
config_manager = ConfigurationManager()
|
|
22
|
+
return config_manager.load_config()
|
|
65
23
|
|
|
66
24
|
def safe_import_registry():
|
|
67
25
|
"""Safely import adapter registry with fallback."""
|
|
@@ -77,24 +35,27 @@ def safe_import_registry():
|
|
|
77
35
|
return MockRegistry
|
|
78
36
|
|
|
79
37
|
def safe_import_queue_manager():
|
|
80
|
-
"""Safely import
|
|
38
|
+
"""Safely import worker manager with fallback."""
|
|
81
39
|
try:
|
|
82
|
-
from ..queue.manager import
|
|
40
|
+
from ..queue.manager import WorkerManager as RealWorkerManager
|
|
83
41
|
|
|
84
|
-
# Test if the real
|
|
42
|
+
# Test if the real worker manager works
|
|
85
43
|
try:
|
|
86
|
-
|
|
44
|
+
wm = RealWorkerManager()
|
|
87
45
|
# Test a basic operation
|
|
88
|
-
|
|
89
|
-
return
|
|
46
|
+
wm.get_status()
|
|
47
|
+
return RealWorkerManager
|
|
90
48
|
except Exception:
|
|
91
|
-
# Real
|
|
49
|
+
# Real worker manager failed, use fallback
|
|
92
50
|
pass
|
|
93
51
|
|
|
94
52
|
except ImportError:
|
|
95
53
|
pass
|
|
96
54
|
|
|
97
|
-
class
|
|
55
|
+
class MockWorkerManager:
|
|
56
|
+
def get_status(self):
|
|
57
|
+
return {"running": False, "pid": None, "status": "fallback_mode"}
|
|
58
|
+
|
|
98
59
|
def get_worker_status(self):
|
|
99
60
|
return {"running": False, "pid": None, "status": "fallback_mode"}
|
|
100
61
|
|
|
@@ -104,12 +65,11 @@ def safe_import_queue_manager():
|
|
|
104
65
|
def health_check(self):
|
|
105
66
|
return {"status": "degraded", "score": 50, "details": "Running in fallback mode"}
|
|
106
67
|
|
|
107
|
-
return
|
|
68
|
+
return MockWorkerManager
|
|
108
69
|
|
|
109
70
|
# Initialize with safe imports
|
|
110
|
-
get_config = safe_import_config()
|
|
111
71
|
AdapterRegistry = safe_import_registry()
|
|
112
|
-
|
|
72
|
+
WorkerManager = safe_import_queue_manager()
|
|
113
73
|
|
|
114
74
|
console = Console()
|
|
115
75
|
logger = logging.getLogger(__name__)
|
|
@@ -126,29 +86,20 @@ class SystemDiagnostics:
|
|
|
126
86
|
|
|
127
87
|
try:
|
|
128
88
|
self.config = get_config()
|
|
129
|
-
|
|
130
|
-
if hasattr(self.config, '__class__') and 'Mock' in self.config.__class__.__name__:
|
|
131
|
-
self.config_available = False
|
|
132
|
-
self.warnings.append("Configuration system using fallback mode")
|
|
133
|
-
else:
|
|
134
|
-
self.config_available = True
|
|
89
|
+
self.config_available = True
|
|
135
90
|
except Exception as e:
|
|
136
91
|
self.config = None
|
|
137
92
|
self.config_available = False
|
|
138
|
-
console.print(f"
|
|
93
|
+
console.print(f"❌ Could not load configuration: {e}")
|
|
94
|
+
raise e
|
|
139
95
|
|
|
140
96
|
try:
|
|
141
|
-
self.
|
|
142
|
-
|
|
143
|
-
if hasattr(self.queue_manager, '__class__') and 'Mock' in self.queue_manager.__class__.__name__:
|
|
144
|
-
self.queue_available = False
|
|
145
|
-
self.warnings.append("Queue system using fallback mode")
|
|
146
|
-
else:
|
|
147
|
-
self.queue_available = True
|
|
97
|
+
self.worker_manager = WorkerManager()
|
|
98
|
+
self.queue_available = True
|
|
148
99
|
except Exception as e:
|
|
149
|
-
self.
|
|
100
|
+
self.worker_manager = None
|
|
150
101
|
self.queue_available = False
|
|
151
|
-
console.print(f"⚠️ Could not initialize
|
|
102
|
+
console.print(f"⚠️ Could not initialize worker manager: {e}")
|
|
152
103
|
|
|
153
104
|
async def run_full_diagnosis(self) -> Dict[str, Any]:
|
|
154
105
|
"""Run complete system diagnosis and return detailed report."""
|
|
@@ -234,25 +185,27 @@ class SystemDiagnostics:
|
|
|
234
185
|
return config_status
|
|
235
186
|
|
|
236
187
|
try:
|
|
237
|
-
# Check adapter configurations
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
188
|
+
# Check adapter configurations using the same approach as working commands
|
|
189
|
+
from .utils import CommonPatterns
|
|
190
|
+
raw_config = CommonPatterns.load_config()
|
|
191
|
+
adapters_config = raw_config.get("adapters", {})
|
|
192
|
+
config_status["adapters_configured"] = len(adapters_config)
|
|
193
|
+
config_status["default_adapter"] = raw_config.get("default_adapter")
|
|
194
|
+
|
|
195
|
+
if not adapters_config:
|
|
243
196
|
issue = "No adapters configured"
|
|
244
197
|
config_status["issues"].append(issue)
|
|
245
198
|
config_status["status"] = "critical"
|
|
246
199
|
self.issues.append(issue)
|
|
247
200
|
console.print(f"❌ {issue}")
|
|
248
201
|
else:
|
|
249
|
-
console.print(f"✅ {len(
|
|
202
|
+
console.print(f"✅ {len(adapters_config)} adapter(s) configured")
|
|
250
203
|
|
|
251
204
|
# Check each adapter configuration
|
|
252
|
-
for name, adapter_config in
|
|
205
|
+
for name, adapter_config in adapters_config.items():
|
|
253
206
|
try:
|
|
254
|
-
|
|
255
|
-
adapter =
|
|
207
|
+
# Use the same adapter creation approach as working commands
|
|
208
|
+
adapter = CommonPatterns.get_adapter(override_adapter=name)
|
|
256
209
|
|
|
257
210
|
# Test adapter validation if available
|
|
258
211
|
if hasattr(adapter, 'validate_credentials'):
|
|
@@ -295,17 +248,15 @@ class SystemDiagnostics:
|
|
|
295
248
|
}
|
|
296
249
|
|
|
297
250
|
try:
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
adapter_type = adapter_config.type.value if hasattr(adapter_config, 'type') else "unknown"
|
|
308
|
-
config_dict = adapter_config.dict() if hasattr(adapter_config, 'dict') else adapter_config
|
|
251
|
+
# Use the same configuration loading approach as working commands
|
|
252
|
+
from .utils import CommonPatterns
|
|
253
|
+
raw_config = CommonPatterns.load_config()
|
|
254
|
+
adapters_config = raw_config.get("adapters", {})
|
|
255
|
+
adapter_status["total_adapters"] = len(adapters_config)
|
|
256
|
+
|
|
257
|
+
for name, adapter_config in adapters_config.items():
|
|
258
|
+
adapter_type = adapter_config.get("type", name)
|
|
259
|
+
config_dict = adapter_config
|
|
309
260
|
|
|
310
261
|
details = {
|
|
311
262
|
"type": adapter_type,
|
|
@@ -315,18 +266,9 @@ class SystemDiagnostics:
|
|
|
315
266
|
}
|
|
316
267
|
|
|
317
268
|
try:
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
except ImportError:
|
|
322
|
-
details["status"] = "failed"
|
|
323
|
-
details["error"] = "AdapterRegistry not available"
|
|
324
|
-
adapter_status["failed_adapters"] += 1
|
|
325
|
-
adapter_status["adapter_details"][name] = details
|
|
326
|
-
continue
|
|
327
|
-
|
|
328
|
-
adapter_class = AdapterRegistry.get_adapter(adapter_type)
|
|
329
|
-
adapter = adapter_class(config_dict)
|
|
269
|
+
# Use the same adapter creation approach as working commands
|
|
270
|
+
from .utils import CommonPatterns
|
|
271
|
+
adapter = CommonPatterns.get_adapter(override_adapter=adapter_type)
|
|
330
272
|
|
|
331
273
|
# Test basic adapter functionality
|
|
332
274
|
test_start = datetime.now()
|
|
@@ -392,7 +334,7 @@ class SystemDiagnostics:
|
|
|
392
334
|
|
|
393
335
|
# Test 1: Check current worker status
|
|
394
336
|
console.print("🔍 Checking current worker status...")
|
|
395
|
-
worker_status = self.
|
|
337
|
+
worker_status = self.worker_manager.get_status()
|
|
396
338
|
queue_status["worker_running"] = worker_status.get("running", False)
|
|
397
339
|
queue_status["worker_pid"] = worker_status.get("pid")
|
|
398
340
|
|
|
@@ -416,7 +358,7 @@ class SystemDiagnostics:
|
|
|
416
358
|
|
|
417
359
|
# Test 3: Get queue statistics
|
|
418
360
|
console.print("🔍 Analyzing queue statistics...")
|
|
419
|
-
stats = self.
|
|
361
|
+
stats = self.worker_manager.queue.get_stats()
|
|
420
362
|
queue_status["queue_stats"] = stats
|
|
421
363
|
|
|
422
364
|
total_items = stats.get("total", 0)
|
|
@@ -479,11 +421,11 @@ class SystemDiagnostics:
|
|
|
479
421
|
}
|
|
480
422
|
|
|
481
423
|
try:
|
|
482
|
-
# Try to start worker using the
|
|
483
|
-
if hasattr(self.
|
|
484
|
-
result =
|
|
485
|
-
test_result["success"] =
|
|
486
|
-
test_result["details"] = "Worker started successfully"
|
|
424
|
+
# Try to start worker using the worker manager
|
|
425
|
+
if hasattr(self.worker_manager, 'start'):
|
|
426
|
+
result = self.worker_manager.start()
|
|
427
|
+
test_result["success"] = result
|
|
428
|
+
test_result["details"] = "Worker started successfully" if result else "Worker failed to start"
|
|
487
429
|
else:
|
|
488
430
|
# Try alternative method - use CLI command
|
|
489
431
|
import subprocess
|
|
@@ -518,6 +460,7 @@ class SystemDiagnostics:
|
|
|
518
460
|
try:
|
|
519
461
|
# Test creating a simple queue item (diagnostic test)
|
|
520
462
|
from ..core.models import Task, Priority
|
|
463
|
+
from ..queue.queue import Queue
|
|
521
464
|
|
|
522
465
|
test_task = Task(
|
|
523
466
|
title="[DIAGNOSTIC TEST] Queue functionality test",
|
|
@@ -525,13 +468,15 @@ class SystemDiagnostics:
|
|
|
525
468
|
priority=Priority.LOW
|
|
526
469
|
)
|
|
527
470
|
|
|
528
|
-
# Try to queue the test task
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
471
|
+
# Try to queue the test task using the correct Queue.add() method
|
|
472
|
+
queue = Queue()
|
|
473
|
+
queue_id = queue.add(
|
|
474
|
+
ticket_data=test_task.model_dump(),
|
|
475
|
+
adapter="aitrackdown",
|
|
476
|
+
operation="create"
|
|
477
|
+
)
|
|
478
|
+
test_result["success"] = True
|
|
479
|
+
test_result["details"] = f"Test task queued successfully: {queue_id}"
|
|
535
480
|
|
|
536
481
|
except Exception as e:
|
|
537
482
|
test_result["error"] = str(e)
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -144,6 +144,83 @@ def load_config(project_dir: Optional[Path] = None) -> dict:
|
|
|
144
144
|
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
|
145
145
|
|
|
146
146
|
|
|
147
|
+
def _discover_from_env_files() -> Optional[str]:
|
|
148
|
+
"""Discover adapter configuration from .env or .env.local files.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Adapter name if discovered, None otherwise
|
|
152
|
+
"""
|
|
153
|
+
import os
|
|
154
|
+
import logging
|
|
155
|
+
from pathlib import Path
|
|
156
|
+
|
|
157
|
+
logger = logging.getLogger(__name__)
|
|
158
|
+
|
|
159
|
+
# Check .env.local first, then .env
|
|
160
|
+
env_files = [".env.local", ".env"]
|
|
161
|
+
|
|
162
|
+
for env_file in env_files:
|
|
163
|
+
env_path = Path.cwd() / env_file
|
|
164
|
+
if env_path.exists():
|
|
165
|
+
try:
|
|
166
|
+
# Simple .env parsing (key=value format)
|
|
167
|
+
env_vars = {}
|
|
168
|
+
with open(env_path, 'r') as f:
|
|
169
|
+
for line in f:
|
|
170
|
+
line = line.strip()
|
|
171
|
+
if line and not line.startswith('#') and '=' in line:
|
|
172
|
+
key, value = line.split('=', 1)
|
|
173
|
+
env_vars[key.strip()] = value.strip().strip('"\'')
|
|
174
|
+
|
|
175
|
+
# Check for adapter-specific variables
|
|
176
|
+
if env_vars.get("LINEAR_API_KEY"):
|
|
177
|
+
logger.info(f"Discovered Linear configuration in {env_file}")
|
|
178
|
+
return "linear"
|
|
179
|
+
elif env_vars.get("GITHUB_TOKEN"):
|
|
180
|
+
logger.info(f"Discovered GitHub configuration in {env_file}")
|
|
181
|
+
return "github"
|
|
182
|
+
elif env_vars.get("JIRA_SERVER"):
|
|
183
|
+
logger.info(f"Discovered JIRA configuration in {env_file}")
|
|
184
|
+
return "jira"
|
|
185
|
+
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(f"Could not read {env_file}: {e}")
|
|
188
|
+
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _save_adapter_to_config(adapter_name: str) -> None:
|
|
193
|
+
"""Save adapter configuration to config file.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
adapter_name: Name of the adapter to save as default
|
|
197
|
+
"""
|
|
198
|
+
import logging
|
|
199
|
+
|
|
200
|
+
logger = logging.getLogger(__name__)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
config = load_config()
|
|
204
|
+
config["default_adapter"] = adapter_name
|
|
205
|
+
|
|
206
|
+
# Ensure adapters section exists
|
|
207
|
+
if "adapters" not in config:
|
|
208
|
+
config["adapters"] = {}
|
|
209
|
+
|
|
210
|
+
# Add basic adapter config if not exists
|
|
211
|
+
if adapter_name not in config["adapters"]:
|
|
212
|
+
if adapter_name == "aitrackdown":
|
|
213
|
+
config["adapters"][adapter_name] = {"base_path": ".aitrackdown"}
|
|
214
|
+
else:
|
|
215
|
+
config["adapters"][adapter_name] = {"type": adapter_name}
|
|
216
|
+
|
|
217
|
+
save_config(config)
|
|
218
|
+
logger.info(f"Saved {adapter_name} as default adapter")
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.warning(f"Could not save adapter configuration: {e}")
|
|
222
|
+
|
|
223
|
+
|
|
147
224
|
def save_config(config: dict) -> None:
|
|
148
225
|
"""Save configuration to project-local config file ONLY.
|
|
149
226
|
|
|
@@ -369,11 +446,19 @@ def init(
|
|
|
369
446
|
if discovered and adapter_type != "aitrackdown":
|
|
370
447
|
discovered_adapter = discovered.get_adapter_by_type(adapter_type)
|
|
371
448
|
if discovered_adapter:
|
|
372
|
-
|
|
449
|
+
adapter_config = discovered_adapter.config.copy()
|
|
450
|
+
# Ensure the config has the correct 'type' field
|
|
451
|
+
adapter_config["type"] = adapter_type
|
|
452
|
+
# Remove 'adapter' field if present (legacy)
|
|
453
|
+
adapter_config.pop("adapter", None)
|
|
454
|
+
config["adapters"][adapter_type] = adapter_config
|
|
373
455
|
|
|
374
456
|
# 4. Handle manual configuration for specific adapters
|
|
375
457
|
if adapter_type == "aitrackdown":
|
|
376
|
-
config["adapters"]["aitrackdown"] = {
|
|
458
|
+
config["adapters"]["aitrackdown"] = {
|
|
459
|
+
"type": "aitrackdown",
|
|
460
|
+
"base_path": base_path or ".aitrackdown"
|
|
461
|
+
}
|
|
377
462
|
|
|
378
463
|
elif adapter_type == "linear":
|
|
379
464
|
# If not auto-discovered, build from CLI params
|
|
@@ -395,6 +480,7 @@ def init(
|
|
|
395
480
|
)
|
|
396
481
|
|
|
397
482
|
if linear_config:
|
|
483
|
+
linear_config["type"] = "linear"
|
|
398
484
|
config["adapters"]["linear"] = linear_config
|
|
399
485
|
|
|
400
486
|
elif adapter_type == "jira":
|
|
@@ -926,11 +1012,25 @@ def create(
|
|
|
926
1012
|
console.print(f"[yellow] • {alert['message']}[/yellow]")
|
|
927
1013
|
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
928
1014
|
|
|
929
|
-
# Get the adapter name
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1015
|
+
# Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
|
|
1016
|
+
if adapter:
|
|
1017
|
+
# Priority 1: Command-line argument - save to config for future use
|
|
1018
|
+
adapter_name = adapter.value
|
|
1019
|
+
_save_adapter_to_config(adapter_name)
|
|
1020
|
+
else:
|
|
1021
|
+
# Priority 2: Check existing config
|
|
1022
|
+
config = load_config()
|
|
1023
|
+
adapter_name = config.get("default_adapter")
|
|
1024
|
+
|
|
1025
|
+
if not adapter_name or adapter_name == "aitrackdown":
|
|
1026
|
+
# Priority 3: Check .env files and save if found
|
|
1027
|
+
env_adapter = _discover_from_env_files()
|
|
1028
|
+
if env_adapter:
|
|
1029
|
+
adapter_name = env_adapter
|
|
1030
|
+
_save_adapter_to_config(adapter_name)
|
|
1031
|
+
else:
|
|
1032
|
+
# Priority 4: Default
|
|
1033
|
+
adapter_name = "aitrackdown"
|
|
934
1034
|
|
|
935
1035
|
# Create task data
|
|
936
1036
|
task_data = {
|
mcp_ticketer/cli/utils.py
CHANGED
|
@@ -31,7 +31,7 @@ class CommonPatterns:
|
|
|
31
31
|
|
|
32
32
|
@staticmethod
|
|
33
33
|
def load_config() -> dict:
|
|
34
|
-
"""Load configuration from project-local config file
|
|
34
|
+
"""Load configuration from project-local config file with environment discovery fallback.
|
|
35
35
|
|
|
36
36
|
SECURITY: This method ONLY reads from the current project directory
|
|
37
37
|
to prevent configuration leakage across projects. It will NEVER read
|
|
@@ -39,7 +39,8 @@ class CommonPatterns:
|
|
|
39
39
|
|
|
40
40
|
Resolution order:
|
|
41
41
|
1. Project-specific config (.mcp-ticketer/config.json in cwd)
|
|
42
|
-
2.
|
|
42
|
+
2. Environment discovery (environment variables and .env files in cwd)
|
|
43
|
+
3. Default to aitrackdown adapter
|
|
43
44
|
|
|
44
45
|
Returns:
|
|
45
46
|
Configuration dictionary with adapter and config keys.
|
|
@@ -77,6 +78,39 @@ class CommonPatterns:
|
|
|
77
78
|
f"[yellow]Warning: Could not load project config: {e}[/yellow]"
|
|
78
79
|
)
|
|
79
80
|
|
|
81
|
+
# Try environment discovery as fallback
|
|
82
|
+
try:
|
|
83
|
+
from ..core.config import ConfigurationManager
|
|
84
|
+
config_manager = ConfigurationManager()
|
|
85
|
+
app_config = config_manager.load_config()
|
|
86
|
+
|
|
87
|
+
# Convert AppConfig to legacy dict format for CLI compatibility
|
|
88
|
+
enabled_adapters = app_config.get_enabled_adapters()
|
|
89
|
+
if enabled_adapters:
|
|
90
|
+
# Use the first enabled adapter as default
|
|
91
|
+
default_adapter = app_config.default_adapter or list(enabled_adapters.keys())[0]
|
|
92
|
+
|
|
93
|
+
# Convert to legacy format
|
|
94
|
+
legacy_config = {
|
|
95
|
+
"default_adapter": default_adapter,
|
|
96
|
+
"adapters": {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Convert adapter configs to dict format
|
|
100
|
+
for name, adapter_config in enabled_adapters.items():
|
|
101
|
+
if hasattr(adapter_config, 'model_dump'):
|
|
102
|
+
legacy_config["adapters"][name] = adapter_config.model_dump(exclude_none=False)
|
|
103
|
+
elif hasattr(adapter_config, 'dict'):
|
|
104
|
+
legacy_config["adapters"][name] = adapter_config.dict()
|
|
105
|
+
else:
|
|
106
|
+
legacy_config["adapters"][name] = adapter_config
|
|
107
|
+
|
|
108
|
+
logger.info(f"Loaded configuration from environment discovery: {list(enabled_adapters.keys())}")
|
|
109
|
+
return legacy_config
|
|
110
|
+
|
|
111
|
+
except Exception as e:
|
|
112
|
+
logger.warning(f"Environment discovery failed: {e}")
|
|
113
|
+
|
|
80
114
|
# Default to aitrackdown with local base path
|
|
81
115
|
logger.info("No project-local config found, defaulting to aitrackdown adapter")
|
|
82
116
|
return {"adapter": "aitrackdown", "config": {"base_path": ".aitrackdown"}}
|
mcp_ticketer/core/config.py
CHANGED
|
@@ -118,9 +118,17 @@ class LinearConfig(BaseAdapterConfig):
|
|
|
118
118
|
type: AdapterType = AdapterType.LINEAR
|
|
119
119
|
api_key: Optional[str] = Field(None, env="LINEAR_API_KEY")
|
|
120
120
|
workspace: Optional[str] = None
|
|
121
|
-
team_key: str
|
|
121
|
+
team_key: Optional[str] = None # Short team key like "BTA"
|
|
122
|
+
team_id: Optional[str] = None # UUID team identifier
|
|
122
123
|
api_url: str = "https://api.linear.app/graphql"
|
|
123
124
|
|
|
125
|
+
@model_validator(mode="after")
|
|
126
|
+
def validate_team_identifier(self):
|
|
127
|
+
"""Ensure either team_key or team_id is provided."""
|
|
128
|
+
if not self.team_key and not self.team_id:
|
|
129
|
+
raise ValueError("Either team_key or team_id is required")
|
|
130
|
+
return self
|
|
131
|
+
|
|
124
132
|
@field_validator("api_key", mode="before")
|
|
125
133
|
@classmethod
|
|
126
134
|
def validate_api_key(cls, v):
|
|
@@ -284,9 +292,11 @@ class ConfigurationManager:
|
|
|
284
292
|
config_data = self._load_config_file(self._config_file_paths[0])
|
|
285
293
|
logger.info(f"Loaded configuration from: {self._config_file_paths[0]}")
|
|
286
294
|
else:
|
|
287
|
-
# No config file found -
|
|
288
|
-
|
|
289
|
-
|
|
295
|
+
# No config file found - use empty config
|
|
296
|
+
config_data = {"adapters": {}, "default_adapter": None}
|
|
297
|
+
|
|
298
|
+
# Use saved configuration only - no automatic discovery
|
|
299
|
+
# Discovery should be done explicitly via 'mcp-ticketer init' or 'mcp-ticketer discover'
|
|
290
300
|
|
|
291
301
|
# Parse adapter configurations
|
|
292
302
|
if "adapters" in config_data:
|
|
@@ -294,13 +304,21 @@ class ConfigurationManager:
|
|
|
294
304
|
for name, adapter_config in config_data["adapters"].items():
|
|
295
305
|
adapter_type = adapter_config.get("type", "").lower()
|
|
296
306
|
|
|
307
|
+
# If no type specified, try to infer from adapter name
|
|
308
|
+
if not adapter_type:
|
|
309
|
+
adapter_type = name.lower()
|
|
310
|
+
|
|
297
311
|
if adapter_type == "github":
|
|
312
|
+
adapter_config["type"] = "github"
|
|
298
313
|
parsed_adapters[name] = GitHubConfig(**adapter_config)
|
|
299
314
|
elif adapter_type == "jira":
|
|
315
|
+
adapter_config["type"] = "jira"
|
|
300
316
|
parsed_adapters[name] = JiraConfig(**adapter_config)
|
|
301
317
|
elif adapter_type == "linear":
|
|
318
|
+
adapter_config["type"] = "linear"
|
|
302
319
|
parsed_adapters[name] = LinearConfig(**adapter_config)
|
|
303
320
|
elif adapter_type == "aitrackdown":
|
|
321
|
+
adapter_config["type"] = "aitrackdown"
|
|
304
322
|
parsed_adapters[name] = AITrackdownConfig(**adapter_config)
|
|
305
323
|
else:
|
|
306
324
|
logger.warning(
|
|
@@ -335,10 +353,10 @@ class ConfigurationManager:
|
|
|
335
353
|
def _discover_from_environment(self) -> dict[str, Any]:
|
|
336
354
|
"""Discover configuration from environment variables."""
|
|
337
355
|
try:
|
|
338
|
-
from .env_discovery import
|
|
356
|
+
from .env_discovery import EnvDiscovery
|
|
339
357
|
|
|
340
|
-
discovery =
|
|
341
|
-
discovered = discovery.
|
|
358
|
+
discovery = EnvDiscovery()
|
|
359
|
+
discovered = discovery.discover()
|
|
342
360
|
|
|
343
361
|
if not discovered.adapters:
|
|
344
362
|
logger.info("No adapters discovered from environment variables")
|
|
@@ -362,36 +380,24 @@ class ConfigurationManager:
|
|
|
362
380
|
|
|
363
381
|
for adapter in discovered.adapters:
|
|
364
382
|
adapter_config = {
|
|
365
|
-
"type": adapter.
|
|
383
|
+
"type": adapter.adapter_type,
|
|
366
384
|
"enabled": True
|
|
367
385
|
}
|
|
368
386
|
|
|
369
|
-
# Add adapter-specific configuration
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"repo": adapter.credentials.get("repo"),
|
|
380
|
-
"owner": adapter.credentials.get("owner")
|
|
381
|
-
})
|
|
382
|
-
elif adapter.type.value == "jira":
|
|
383
|
-
adapter_config.update({
|
|
384
|
-
"server": adapter.credentials.get("server"),
|
|
385
|
-
"email": adapter.credentials.get("email"),
|
|
386
|
-
"api_token": adapter.credentials.get("api_token"),
|
|
387
|
-
"project_key": adapter.credentials.get("project_key")
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
config_data["adapters"][adapter.name] = adapter_config
|
|
387
|
+
# Add adapter-specific configuration from discovered config
|
|
388
|
+
adapter_config.update(adapter.config)
|
|
389
|
+
|
|
390
|
+
# Ensure type is set correctly (remove 'adapter' key if present)
|
|
391
|
+
if 'adapter' in adapter_config:
|
|
392
|
+
del adapter_config['adapter']
|
|
393
|
+
|
|
394
|
+
# Use adapter type as the key name
|
|
395
|
+
adapter_name = adapter.adapter_type
|
|
396
|
+
config_data["adapters"][adapter_name] = adapter_config
|
|
391
397
|
|
|
392
398
|
# Set first discovered adapter as default
|
|
393
399
|
if config_data["default_adapter"] is None:
|
|
394
|
-
config_data["default_adapter"] = adapter.
|
|
400
|
+
config_data["default_adapter"] = adapter.adapter_type
|
|
395
401
|
|
|
396
402
|
logger.info(f"Discovered {len(config_data['adapters'])} adapter(s) from environment")
|
|
397
403
|
return config_data
|
|
@@ -30,8 +30,10 @@ LINEAR_KEY_PATTERNS = [
|
|
|
30
30
|
|
|
31
31
|
LINEAR_TEAM_PATTERNS = [
|
|
32
32
|
"LINEAR_TEAM_ID",
|
|
33
|
+
"LINEAR_TEAM_KEY", # Added support for team key (e.g., "BTA")
|
|
33
34
|
"LINEAR_TEAM",
|
|
34
35
|
"MCP_TICKETER_LINEAR_TEAM_ID",
|
|
36
|
+
"MCP_TICKETER_LINEAR_TEAM_KEY",
|
|
35
37
|
]
|
|
36
38
|
|
|
37
39
|
LINEAR_PROJECT_PATTERNS = [
|
|
@@ -210,7 +212,11 @@ class EnvDiscovery:
|
|
|
210
212
|
return result
|
|
211
213
|
|
|
212
214
|
def _load_env_files(self, result: DiscoveryResult) -> dict[str, str]:
|
|
213
|
-
"""Load environment variables from files.
|
|
215
|
+
"""Load environment variables from files and actual environment.
|
|
216
|
+
|
|
217
|
+
Priority order (highest to lowest):
|
|
218
|
+
1. .env files (highest priority)
|
|
219
|
+
2. Environment variables (lowest priority)
|
|
214
220
|
|
|
215
221
|
Args:
|
|
216
222
|
result: DiscoveryResult to update with found files
|
|
@@ -221,7 +227,15 @@ class EnvDiscovery:
|
|
|
221
227
|
"""
|
|
222
228
|
merged_env: dict[str, str] = {}
|
|
223
229
|
|
|
224
|
-
#
|
|
230
|
+
# First, load from actual environment variables (lowest priority)
|
|
231
|
+
import os
|
|
232
|
+
actual_env = {k: v for k, v in os.environ.items() if v}
|
|
233
|
+
merged_env.update(actual_env)
|
|
234
|
+
if actual_env:
|
|
235
|
+
result.env_files_found.append("environment")
|
|
236
|
+
logger.debug(f"Loaded {len(actual_env)} variables from environment")
|
|
237
|
+
|
|
238
|
+
# Load files in reverse order (higher priority than environment)
|
|
225
239
|
for env_file in reversed(self.ENV_FILE_ORDER):
|
|
226
240
|
file_path = self.project_path / env_file
|
|
227
241
|
if file_path.exists():
|
|
@@ -282,13 +296,19 @@ class EnvDiscovery:
|
|
|
282
296
|
missing_fields: list[str] = []
|
|
283
297
|
confidence = 0.6 # Has API key
|
|
284
298
|
|
|
285
|
-
# Extract team
|
|
286
|
-
|
|
287
|
-
if
|
|
288
|
-
|
|
299
|
+
# Extract team identifier (either team_id or team_key is required)
|
|
300
|
+
team_identifier = self._find_key_value(env_vars, LINEAR_TEAM_PATTERNS)
|
|
301
|
+
if team_identifier:
|
|
302
|
+
# Determine if it's a team_id (UUID format) or team_key (short string)
|
|
303
|
+
if len(team_identifier) > 20 and '-' in team_identifier:
|
|
304
|
+
# Looks like a UUID (team_id)
|
|
305
|
+
config["team_id"] = team_identifier
|
|
306
|
+
else:
|
|
307
|
+
# Looks like a short key (team_key)
|
|
308
|
+
config["team_key"] = team_identifier
|
|
289
309
|
confidence += 0.3
|
|
290
310
|
else:
|
|
291
|
-
missing_fields.append("team_id (
|
|
311
|
+
missing_fields.append("team_id or team_key (required)")
|
|
292
312
|
|
|
293
313
|
# Extract project ID (optional)
|
|
294
314
|
project_id = self._find_key_value(env_vars, LINEAR_PROJECT_PATTERNS)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-ticketer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.36
|
|
4
4
|
Summary: Universal ticket management interface for AI agents with MCP support
|
|
5
5
|
Author-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
6
6
|
Maintainer-email: MCP Ticketer Team <support@mcp-ticketer.io>
|
|
@@ -38,6 +38,7 @@ Requires-Dist: httpx>=0.25.0
|
|
|
38
38
|
Requires-Dist: psutil>=5.9.0
|
|
39
39
|
Requires-Dist: pydantic>=2.0
|
|
40
40
|
Requires-Dist: python-dotenv>=1.0.0
|
|
41
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
41
42
|
Requires-Dist: rich>=13.0.0
|
|
42
43
|
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
|
|
43
44
|
Requires-Dist: tomli-w>=1.0.0
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
mcp_ticketer/__init__.py,sha256=Xx4WaprO5PXhVPbYi1L6tBmwmJMkYS-lMyG4ieN6QP0,717
|
|
2
|
-
mcp_ticketer/__version__.py,sha256=
|
|
2
|
+
mcp_ticketer/__version__.py,sha256=MUs8plklj0TdV4_KwKETYDo6huvcxKAeEORU6FUFWJ0,1118
|
|
3
3
|
mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
mcp_ticketer/adapters/__init__.py,sha256=B5DFllWn23hkhmrLykNO5uMMSdcFuuPHXyLw_jyFzuE,358
|
|
5
5
|
mcp_ticketer/adapters/aitrackdown.py,sha256=stlbge8K6w-EyQkw_vEQNSXQgCOWN5tOlQUgGWZQNMQ,17936
|
|
@@ -13,19 +13,19 @@ mcp_ticketer/cli/__init__.py,sha256=l9Q8iKmfGkTu0cssHBVqNZTsL4eAtFzOB25AED_0G6g,
|
|
|
13
13
|
mcp_ticketer/cli/auggie_configure.py,sha256=MXKzLtqe3K_UTQ2GacHAWbvf_B0779KL325smiAKE0Q,8212
|
|
14
14
|
mcp_ticketer/cli/codex_configure.py,sha256=xDppHouT6_-cYXswyAggoPX5bSlRXMvCoM_x9PQ-42A,9086
|
|
15
15
|
mcp_ticketer/cli/configure.py,sha256=BsA_pSHQMQS0t1bJO_wMM8LWsd5sWJDASjEPRHvwC18,16198
|
|
16
|
-
mcp_ticketer/cli/diagnostics.py,sha256=
|
|
16
|
+
mcp_ticketer/cli/diagnostics.py,sha256=AC7cMQHVWdHfrYH2Y1tkhmRezM2-mID5E_Dhfil7F3U,28923
|
|
17
17
|
mcp_ticketer/cli/discover.py,sha256=AF_qlQc1Oo0UkWayoF5pmRChS5J3fJjH6f2YZzd_k8w,13188
|
|
18
18
|
mcp_ticketer/cli/gemini_configure.py,sha256=ZNSA1lBW-itVToza-JxW95Po7daVXKiZAh7lp6pmXMU,9343
|
|
19
|
-
mcp_ticketer/cli/main.py,sha256=
|
|
19
|
+
mcp_ticketer/cli/main.py,sha256=pxx3bKMkZdCUF6Cirk_U-cDMsBGKXCcr6WhL9tqLsKw,58555
|
|
20
20
|
mcp_ticketer/cli/mcp_configure.py,sha256=RzV50UjXgOmvMp-9S0zS39psuvjffVByaMrqrUaAGAM,9594
|
|
21
21
|
mcp_ticketer/cli/migrate_config.py,sha256=MYsr_C5ZxsGg0P13etWTWNrJ_lc6ElRCkzfQADYr3DM,5956
|
|
22
22
|
mcp_ticketer/cli/queue_commands.py,sha256=mm-3H6jmkUGJDyU_E46o9iRpek8tvFCm77F19OtHiZI,7884
|
|
23
23
|
mcp_ticketer/cli/simple_health.py,sha256=FIMHbrSNHpNJHXx7wtH8HzQXmPlcF9HQE9ngxTbxhMM,8035
|
|
24
|
-
mcp_ticketer/cli/utils.py,sha256=
|
|
24
|
+
mcp_ticketer/cli/utils.py,sha256=RRoUMJbw0isyzlRhZwPcbmYQXl1PGYDx9dy1zRctljA,22789
|
|
25
25
|
mcp_ticketer/core/__init__.py,sha256=eXovsaJymQRP2AwOBuOy6mFtI3I68D7gGenZ5V-IMqo,349
|
|
26
26
|
mcp_ticketer/core/adapter.py,sha256=q64LxOInIno7EIbmuxItf8KEsd-g9grCs__Z4uwZHto,10273
|
|
27
|
-
mcp_ticketer/core/config.py,sha256=
|
|
28
|
-
mcp_ticketer/core/env_discovery.py,sha256=
|
|
27
|
+
mcp_ticketer/core/config.py,sha256=hvn8RoN2BSmYzESKqCvcpKleX0PrGiHVQ5v35nX12Mc,19241
|
|
28
|
+
mcp_ticketer/core/env_discovery.py,sha256=It62C97UBt96CgVbZCql5NQtONmCHMkX3c8W2kEl-Qk,18716
|
|
29
29
|
mcp_ticketer/core/http_client.py,sha256=s5ikMiwEJ8TJjNn73wu3gv3OdAtyBEpAqPnSroRMW2k,13971
|
|
30
30
|
mcp_ticketer/core/mappers.py,sha256=1aG1jFsHTCwmGRVgOlXW-VOSTGzc86gv7qjDfiR1ups,17462
|
|
31
31
|
mcp_ticketer/core/models.py,sha256=DRuJoYbjp9fcPV9GwQfhVcNUB0XmwQB3vuqW8hQWZ_k,6491
|
|
@@ -41,9 +41,9 @@ mcp_ticketer/queue/queue.py,sha256=jSAkYNEIbNH1cbYuF8s6eFuZmXqn8WHXx3mbfMU2Ud8,1
|
|
|
41
41
|
mcp_ticketer/queue/run_worker.py,sha256=_IBezjvhbJJ7gn0evTBIMbSPjvfFZwxEdT-1DLo_bRk,799
|
|
42
42
|
mcp_ticketer/queue/ticket_registry.py,sha256=k8FYg2cFYsI4POb94-o-fTrIVr-ttfi60r0O5YhJYck,15321
|
|
43
43
|
mcp_ticketer/queue/worker.py,sha256=TLXXXTAQT1k9Oiw2WjSd8bzT3rr8TQ8NLt9JBovGQEA,18679
|
|
44
|
-
mcp_ticketer-0.1.
|
|
45
|
-
mcp_ticketer-0.1.
|
|
46
|
-
mcp_ticketer-0.1.
|
|
47
|
-
mcp_ticketer-0.1.
|
|
48
|
-
mcp_ticketer-0.1.
|
|
49
|
-
mcp_ticketer-0.1.
|
|
44
|
+
mcp_ticketer-0.1.36.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
45
|
+
mcp_ticketer-0.1.36.dist-info/METADATA,sha256=d0EDTsEDPLyod1rHUZPowpmH_aWoJUPM7KrNuBs_O4w,13220
|
|
46
|
+
mcp_ticketer-0.1.36.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
mcp_ticketer-0.1.36.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
48
|
+
mcp_ticketer-0.1.36.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
49
|
+
mcp_ticketer-0.1.36.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|