mcp-ticketer 0.1.31__py3-none-any.whl → 0.1.34__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 +253 -42
- mcp_ticketer/cli/main.py +96 -5
- mcp_ticketer/cli/utils.py +36 -2
- mcp_ticketer/core/config.py +53 -31
- mcp_ticketer/core/env_discovery.py +27 -7
- {mcp_ticketer-0.1.31.dist-info → mcp_ticketer-0.1.34.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.31.dist-info → mcp_ticketer-0.1.34.dist-info}/RECORD +12 -12
- {mcp_ticketer-0.1.31.dist-info → mcp_ticketer-0.1.34.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.31.dist-info → mcp_ticketer-0.1.34.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.31.dist-info → mcp_ticketer-0.1.34.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.31.dist-info → mcp_ticketer-0.1.34.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
mcp_ticketer/cli/diagnostics.py
CHANGED
|
@@ -18,22 +18,50 @@ from rich.text import Text
|
|
|
18
18
|
def safe_import_config():
|
|
19
19
|
"""Safely import configuration with fallback."""
|
|
20
20
|
try:
|
|
21
|
-
from ..core.config import get_config
|
|
22
|
-
|
|
21
|
+
from ..core.config import get_config as real_get_config
|
|
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
|
+
|
|
23
32
|
except ImportError:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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}
|
|
28
49
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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"
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
def get_config():
|
|
62
|
+
return MockConfig()
|
|
35
63
|
|
|
36
|
-
|
|
64
|
+
return get_config
|
|
37
65
|
|
|
38
66
|
def safe_import_registry():
|
|
39
67
|
"""Safely import adapter registry with fallback."""
|
|
@@ -51,17 +79,32 @@ def safe_import_registry():
|
|
|
51
79
|
def safe_import_queue_manager():
|
|
52
80
|
"""Safely import queue manager with fallback."""
|
|
53
81
|
try:
|
|
54
|
-
from ..queue.manager import QueueManager
|
|
55
|
-
|
|
82
|
+
from ..queue.manager import QueueManager as RealQueueManager
|
|
83
|
+
|
|
84
|
+
# Test if the real queue manager works
|
|
85
|
+
try:
|
|
86
|
+
qm = RealQueueManager()
|
|
87
|
+
# Test a basic operation
|
|
88
|
+
qm.get_worker_status()
|
|
89
|
+
return RealQueueManager
|
|
90
|
+
except Exception:
|
|
91
|
+
# Real queue manager failed, use fallback
|
|
92
|
+
pass
|
|
93
|
+
|
|
56
94
|
except ImportError:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
class MockQueueManager:
|
|
98
|
+
def get_worker_status(self):
|
|
99
|
+
return {"running": False, "pid": None, "status": "fallback_mode"}
|
|
60
100
|
|
|
61
|
-
|
|
62
|
-
|
|
101
|
+
def get_queue_stats(self):
|
|
102
|
+
return {"total": 0, "failed": 0, "pending": 0, "completed": 0}
|
|
63
103
|
|
|
64
|
-
|
|
104
|
+
def health_check(self):
|
|
105
|
+
return {"status": "degraded", "score": 50, "details": "Running in fallback mode"}
|
|
106
|
+
|
|
107
|
+
return MockQueueManager
|
|
65
108
|
|
|
66
109
|
# Initialize with safe imports
|
|
67
110
|
get_config = safe_import_config()
|
|
@@ -209,7 +252,9 @@ class SystemDiagnostics:
|
|
|
209
252
|
for name, adapter_config in adapters.items():
|
|
210
253
|
try:
|
|
211
254
|
adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
|
|
212
|
-
|
|
255
|
+
# Convert Pydantic model to dict, excluding None values
|
|
256
|
+
config_dict = adapter_config.model_dump(exclude_none=False)
|
|
257
|
+
adapter = adapter_class(config_dict)
|
|
213
258
|
|
|
214
259
|
# Test adapter validation if available
|
|
215
260
|
if hasattr(adapter, 'validate_credentials'):
|
|
@@ -256,16 +301,40 @@ class SystemDiagnostics:
|
|
|
256
301
|
adapter_status["total_adapters"] = len(adapters)
|
|
257
302
|
|
|
258
303
|
for name, adapter_config in adapters.items():
|
|
304
|
+
# Handle both dict and object adapter configs
|
|
305
|
+
if isinstance(adapter_config, dict):
|
|
306
|
+
adapter_type = adapter_config.get("type", "unknown")
|
|
307
|
+
config_dict = adapter_config
|
|
308
|
+
else:
|
|
309
|
+
adapter_type = adapter_config.type.value if hasattr(adapter_config, 'type') else "unknown"
|
|
310
|
+
# Use model_dump for Pydantic v2 compatibility
|
|
311
|
+
if hasattr(adapter_config, 'model_dump'):
|
|
312
|
+
config_dict = adapter_config.model_dump(exclude_none=False)
|
|
313
|
+
elif hasattr(adapter_config, 'dict'):
|
|
314
|
+
config_dict = adapter_config.dict()
|
|
315
|
+
else:
|
|
316
|
+
config_dict = adapter_config
|
|
317
|
+
|
|
259
318
|
details = {
|
|
260
|
-
"type":
|
|
319
|
+
"type": adapter_type,
|
|
261
320
|
"status": "unknown",
|
|
262
321
|
"last_test": None,
|
|
263
322
|
"error": None,
|
|
264
323
|
}
|
|
265
324
|
|
|
266
325
|
try:
|
|
267
|
-
|
|
268
|
-
|
|
326
|
+
# Import AdapterRegistry safely
|
|
327
|
+
try:
|
|
328
|
+
from ..core.registry import AdapterRegistry
|
|
329
|
+
except ImportError:
|
|
330
|
+
details["status"] = "failed"
|
|
331
|
+
details["error"] = "AdapterRegistry not available"
|
|
332
|
+
adapter_status["failed_adapters"] += 1
|
|
333
|
+
adapter_status["adapter_details"][name] = details
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
adapter_class = AdapterRegistry.get_adapter(adapter_type)
|
|
337
|
+
adapter = adapter_class(config_dict)
|
|
269
338
|
|
|
270
339
|
# Test basic adapter functionality
|
|
271
340
|
test_start = datetime.now()
|
|
@@ -303,9 +372,9 @@ class SystemDiagnostics:
|
|
|
303
372
|
return adapter_status
|
|
304
373
|
|
|
305
374
|
async def _diagnose_queue_system(self) -> Dict[str, Any]:
|
|
306
|
-
"""Diagnose queue system health."""
|
|
375
|
+
"""Diagnose queue system health with active testing."""
|
|
307
376
|
console.print("\n⚡ [yellow]Queue System Diagnosis[/yellow]")
|
|
308
|
-
|
|
377
|
+
|
|
309
378
|
queue_status = {
|
|
310
379
|
"worker_running": False,
|
|
311
380
|
"worker_pid": None,
|
|
@@ -313,19 +382,24 @@ class SystemDiagnostics:
|
|
|
313
382
|
"recent_failures": [],
|
|
314
383
|
"failure_rate": 0.0,
|
|
315
384
|
"health_score": 0,
|
|
385
|
+
"worker_start_test": {"attempted": False, "success": False, "error": None},
|
|
386
|
+
"queue_operation_test": {"attempted": False, "success": False, "error": None},
|
|
316
387
|
}
|
|
317
388
|
|
|
318
389
|
try:
|
|
319
390
|
if not self.queue_available:
|
|
320
|
-
warning = "Queue system in fallback mode -
|
|
391
|
+
warning = "Queue system in fallback mode - testing basic functionality"
|
|
321
392
|
self.warnings.append(warning)
|
|
322
393
|
console.print(f"⚠️ {warning}")
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
394
|
+
|
|
395
|
+
# Even in fallback mode, test if we can create a basic queue operation
|
|
396
|
+
test_result = await self._test_basic_queue_functionality()
|
|
397
|
+
queue_status["queue_operation_test"] = test_result
|
|
398
|
+
queue_status["health_score"] = 50 if test_result["success"] else 25
|
|
326
399
|
return queue_status
|
|
327
400
|
|
|
328
|
-
# Check worker status
|
|
401
|
+
# Test 1: Check current worker status
|
|
402
|
+
console.print("🔍 Checking current worker status...")
|
|
329
403
|
worker_status = self.queue_manager.get_worker_status()
|
|
330
404
|
queue_status["worker_running"] = worker_status.get("running", False)
|
|
331
405
|
queue_status["worker_pid"] = worker_status.get("pid")
|
|
@@ -334,21 +408,32 @@ class SystemDiagnostics:
|
|
|
334
408
|
console.print(f"✅ Queue worker running (PID: {queue_status['worker_pid']})")
|
|
335
409
|
self.successes.append("Queue worker is running")
|
|
336
410
|
else:
|
|
337
|
-
|
|
338
|
-
self.issues.append(issue)
|
|
339
|
-
console.print(f"❌ {issue}")
|
|
411
|
+
console.print("⚠️ Queue worker not running - attempting to start...")
|
|
340
412
|
|
|
341
|
-
|
|
413
|
+
# Test 2: Try to start worker
|
|
414
|
+
start_test = await self._test_worker_startup()
|
|
415
|
+
queue_status["worker_start_test"] = start_test
|
|
416
|
+
|
|
417
|
+
if start_test["success"]:
|
|
418
|
+
console.print("✅ Successfully started queue worker")
|
|
419
|
+
queue_status["worker_running"] = True
|
|
420
|
+
self.successes.append("Queue worker started successfully")
|
|
421
|
+
else:
|
|
422
|
+
console.print(f"❌ Failed to start queue worker: {start_test['error']}")
|
|
423
|
+
self.issues.append(f"Queue worker startup failed: {start_test['error']}")
|
|
424
|
+
|
|
425
|
+
# Test 3: Get queue statistics
|
|
426
|
+
console.print("🔍 Analyzing queue statistics...")
|
|
342
427
|
stats = self.queue_manager.get_queue_stats()
|
|
343
428
|
queue_status["queue_stats"] = stats
|
|
344
|
-
|
|
429
|
+
|
|
345
430
|
total_items = stats.get("total", 0)
|
|
346
431
|
failed_items = stats.get("failed", 0)
|
|
347
|
-
|
|
432
|
+
|
|
348
433
|
if total_items > 0:
|
|
349
434
|
failure_rate = (failed_items / total_items) * 100
|
|
350
435
|
queue_status["failure_rate"] = failure_rate
|
|
351
|
-
|
|
436
|
+
|
|
352
437
|
if failure_rate > 50:
|
|
353
438
|
issue = f"High failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})"
|
|
354
439
|
self.issues.append(issue)
|
|
@@ -360,14 +445,30 @@ class SystemDiagnostics:
|
|
|
360
445
|
else:
|
|
361
446
|
console.print(f"✅ Queue failure rate: {failure_rate:.1f}% ({failed_items}/{total_items})")
|
|
362
447
|
|
|
363
|
-
#
|
|
448
|
+
# Test 4: Test actual queue operations
|
|
449
|
+
console.print("🔍 Testing queue operations...")
|
|
450
|
+
operation_test = await self._test_queue_operations()
|
|
451
|
+
queue_status["queue_operation_test"] = operation_test
|
|
452
|
+
|
|
453
|
+
if operation_test["success"]:
|
|
454
|
+
console.print("✅ Queue operations test passed")
|
|
455
|
+
self.successes.append("Queue operations working correctly")
|
|
456
|
+
else:
|
|
457
|
+
console.print(f"❌ Queue operations test failed: {operation_test['error']}")
|
|
458
|
+
self.issues.append(f"Queue operations failed: {operation_test['error']}")
|
|
459
|
+
|
|
460
|
+
# Calculate health score based on actual tests
|
|
364
461
|
health_score = 100
|
|
365
462
|
if not queue_status["worker_running"]:
|
|
366
|
-
health_score -=
|
|
367
|
-
|
|
463
|
+
health_score -= 30
|
|
464
|
+
if not queue_status["worker_start_test"]["success"] and queue_status["worker_start_test"]["attempted"]:
|
|
465
|
+
health_score -= 20
|
|
466
|
+
if not queue_status["queue_operation_test"]["success"]:
|
|
467
|
+
health_score -= 30
|
|
468
|
+
health_score -= min(queue_status["failure_rate"], 20)
|
|
368
469
|
queue_status["health_score"] = max(0, health_score)
|
|
369
470
|
|
|
370
|
-
console.print(f"📊 Queue health score: {queue_status['health_score']}/100")
|
|
471
|
+
console.print(f"📊 Queue health score: {queue_status['health_score']}/100 (based on active testing)")
|
|
371
472
|
|
|
372
473
|
except Exception as e:
|
|
373
474
|
issue = f"Queue system diagnosis failed: {str(e)}"
|
|
@@ -376,6 +477,116 @@ class SystemDiagnostics:
|
|
|
376
477
|
|
|
377
478
|
return queue_status
|
|
378
479
|
|
|
480
|
+
async def _test_worker_startup(self) -> Dict[str, Any]:
|
|
481
|
+
"""Test starting a queue worker."""
|
|
482
|
+
test_result = {
|
|
483
|
+
"attempted": True,
|
|
484
|
+
"success": False,
|
|
485
|
+
"error": None,
|
|
486
|
+
"details": None
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try:
|
|
490
|
+
# Try to start worker using the queue manager
|
|
491
|
+
if hasattr(self.queue_manager, 'start_worker'):
|
|
492
|
+
result = await self.queue_manager.start_worker()
|
|
493
|
+
test_result["success"] = True
|
|
494
|
+
test_result["details"] = "Worker started successfully"
|
|
495
|
+
else:
|
|
496
|
+
# Try alternative method - use CLI command
|
|
497
|
+
import subprocess
|
|
498
|
+
result = subprocess.run(
|
|
499
|
+
["mcp-ticketer", "queue", "worker", "start"],
|
|
500
|
+
capture_output=True,
|
|
501
|
+
text=True,
|
|
502
|
+
timeout=10
|
|
503
|
+
)
|
|
504
|
+
if result.returncode == 0:
|
|
505
|
+
test_result["success"] = True
|
|
506
|
+
test_result["details"] = "Worker started via CLI"
|
|
507
|
+
else:
|
|
508
|
+
test_result["error"] = f"CLI start failed: {result.stderr}"
|
|
509
|
+
|
|
510
|
+
except subprocess.TimeoutExpired:
|
|
511
|
+
test_result["error"] = "Worker startup timed out"
|
|
512
|
+
except Exception as e:
|
|
513
|
+
test_result["error"] = str(e)
|
|
514
|
+
|
|
515
|
+
return test_result
|
|
516
|
+
|
|
517
|
+
async def _test_queue_operations(self) -> Dict[str, Any]:
|
|
518
|
+
"""Test basic queue operations."""
|
|
519
|
+
test_result = {
|
|
520
|
+
"attempted": True,
|
|
521
|
+
"success": False,
|
|
522
|
+
"error": None,
|
|
523
|
+
"details": None
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
try:
|
|
527
|
+
# Test creating a simple queue item (diagnostic test)
|
|
528
|
+
from ..core.models import Task, Priority
|
|
529
|
+
|
|
530
|
+
test_task = Task(
|
|
531
|
+
title="[DIAGNOSTIC TEST] Queue functionality test",
|
|
532
|
+
description="This is a diagnostic test - safe to ignore",
|
|
533
|
+
priority=Priority.LOW
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Try to queue the test task
|
|
537
|
+
if hasattr(self.queue_manager, 'queue_task'):
|
|
538
|
+
queue_id = await self.queue_manager.queue_task("create", test_task, "aitrackdown")
|
|
539
|
+
test_result["success"] = True
|
|
540
|
+
test_result["details"] = f"Test task queued successfully: {queue_id}"
|
|
541
|
+
else:
|
|
542
|
+
test_result["error"] = "Queue manager doesn't support task queuing"
|
|
543
|
+
|
|
544
|
+
except Exception as e:
|
|
545
|
+
test_result["error"] = str(e)
|
|
546
|
+
|
|
547
|
+
return test_result
|
|
548
|
+
|
|
549
|
+
async def _test_basic_queue_functionality(self) -> Dict[str, Any]:
|
|
550
|
+
"""Test basic queue functionality in fallback mode."""
|
|
551
|
+
test_result = {
|
|
552
|
+
"attempted": True,
|
|
553
|
+
"success": False,
|
|
554
|
+
"error": None,
|
|
555
|
+
"details": None
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
# Test if we can at least create a task directly (bypass queue)
|
|
560
|
+
from ..core.models import Task, Priority
|
|
561
|
+
from ..adapters.aitrackdown import AITrackdownAdapter
|
|
562
|
+
|
|
563
|
+
test_task = Task(
|
|
564
|
+
title="[DIAGNOSTIC TEST] Direct adapter test",
|
|
565
|
+
description="Testing direct adapter functionality",
|
|
566
|
+
priority=Priority.LOW
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
# Try direct adapter creation
|
|
570
|
+
adapter_config = {
|
|
571
|
+
"type": "aitrackdown",
|
|
572
|
+
"enabled": True,
|
|
573
|
+
"base_path": "/tmp/mcp-ticketer-diagnostic-test"
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
adapter = AITrackdownAdapter(adapter_config)
|
|
577
|
+
result = await adapter.create(test_task)
|
|
578
|
+
|
|
579
|
+
test_result["success"] = True
|
|
580
|
+
test_result["details"] = f"Direct adapter test passed: {result.id}"
|
|
581
|
+
|
|
582
|
+
# Clean up test
|
|
583
|
+
await adapter.delete(result.id)
|
|
584
|
+
|
|
585
|
+
except Exception as e:
|
|
586
|
+
test_result["error"] = str(e)
|
|
587
|
+
|
|
588
|
+
return test_result
|
|
589
|
+
|
|
379
590
|
async def _analyze_recent_logs(self) -> Dict[str, Any]:
|
|
380
591
|
"""Analyze recent log entries for issues."""
|
|
381
592
|
console.print("\n📝 [yellow]Recent Log Analysis[/yellow]")
|
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
|
|
|
@@ -926,11 +1003,25 @@ def create(
|
|
|
926
1003
|
console.print(f"[yellow] • {alert['message']}[/yellow]")
|
|
927
1004
|
console.print("[yellow]Proceeding with ticket creation...[/yellow]")
|
|
928
1005
|
|
|
929
|
-
# Get the adapter name
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1006
|
+
# Get the adapter name with priority: 1) argument, 2) config, 3) .env files, 4) default
|
|
1007
|
+
if adapter:
|
|
1008
|
+
# Priority 1: Command-line argument - save to config for future use
|
|
1009
|
+
adapter_name = adapter.value
|
|
1010
|
+
_save_adapter_to_config(adapter_name)
|
|
1011
|
+
else:
|
|
1012
|
+
# Priority 2: Check existing config
|
|
1013
|
+
config = load_config()
|
|
1014
|
+
adapter_name = config.get("default_adapter")
|
|
1015
|
+
|
|
1016
|
+
if not adapter_name or adapter_name == "aitrackdown":
|
|
1017
|
+
# Priority 3: Check .env files and save if found
|
|
1018
|
+
env_adapter = _discover_from_env_files()
|
|
1019
|
+
if env_adapter:
|
|
1020
|
+
adapter_name = env_adapter
|
|
1021
|
+
_save_adapter_to_config(adapter_name)
|
|
1022
|
+
else:
|
|
1023
|
+
# Priority 4: Default
|
|
1024
|
+
adapter_name = "aitrackdown"
|
|
934
1025
|
|
|
935
1026
|
# Create task data
|
|
936
1027
|
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,27 @@ 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
|
+
# Always try environment discovery and merge with file-based config
|
|
299
|
+
logger.info("Attempting environment discovery to supplement configuration")
|
|
300
|
+
env_config_data = self._discover_from_environment()
|
|
301
|
+
|
|
302
|
+
# Merge environment-discovered adapters with file-based config
|
|
303
|
+
if env_config_data and "adapters" in env_config_data:
|
|
304
|
+
if "adapters" not in config_data:
|
|
305
|
+
config_data["adapters"] = {}
|
|
306
|
+
|
|
307
|
+
# Add discovered adapters that aren't already configured
|
|
308
|
+
for adapter_name, adapter_config in env_config_data["adapters"].items():
|
|
309
|
+
if adapter_name not in config_data["adapters"]:
|
|
310
|
+
config_data["adapters"][adapter_name] = adapter_config
|
|
311
|
+
logger.info(f"Added environment-discovered adapter: {adapter_name}")
|
|
312
|
+
|
|
313
|
+
# Set default adapter if not already set
|
|
314
|
+
if not config_data.get("default_adapter") and env_config_data.get("default_adapter"):
|
|
315
|
+
config_data["default_adapter"] = env_config_data["default_adapter"]
|
|
290
316
|
|
|
291
317
|
# Parse adapter configurations
|
|
292
318
|
if "adapters" in config_data:
|
|
@@ -294,13 +320,21 @@ class ConfigurationManager:
|
|
|
294
320
|
for name, adapter_config in config_data["adapters"].items():
|
|
295
321
|
adapter_type = adapter_config.get("type", "").lower()
|
|
296
322
|
|
|
323
|
+
# If no type specified, try to infer from adapter name
|
|
324
|
+
if not adapter_type:
|
|
325
|
+
adapter_type = name.lower()
|
|
326
|
+
|
|
297
327
|
if adapter_type == "github":
|
|
328
|
+
adapter_config["type"] = "github"
|
|
298
329
|
parsed_adapters[name] = GitHubConfig(**adapter_config)
|
|
299
330
|
elif adapter_type == "jira":
|
|
331
|
+
adapter_config["type"] = "jira"
|
|
300
332
|
parsed_adapters[name] = JiraConfig(**adapter_config)
|
|
301
333
|
elif adapter_type == "linear":
|
|
334
|
+
adapter_config["type"] = "linear"
|
|
302
335
|
parsed_adapters[name] = LinearConfig(**adapter_config)
|
|
303
336
|
elif adapter_type == "aitrackdown":
|
|
337
|
+
adapter_config["type"] = "aitrackdown"
|
|
304
338
|
parsed_adapters[name] = AITrackdownConfig(**adapter_config)
|
|
305
339
|
else:
|
|
306
340
|
logger.warning(
|
|
@@ -335,10 +369,10 @@ class ConfigurationManager:
|
|
|
335
369
|
def _discover_from_environment(self) -> dict[str, Any]:
|
|
336
370
|
"""Discover configuration from environment variables."""
|
|
337
371
|
try:
|
|
338
|
-
from .env_discovery import
|
|
372
|
+
from .env_discovery import EnvDiscovery
|
|
339
373
|
|
|
340
|
-
discovery =
|
|
341
|
-
discovered = discovery.
|
|
374
|
+
discovery = EnvDiscovery()
|
|
375
|
+
discovered = discovery.discover()
|
|
342
376
|
|
|
343
377
|
if not discovered.adapters:
|
|
344
378
|
logger.info("No adapters discovered from environment variables")
|
|
@@ -362,36 +396,24 @@ class ConfigurationManager:
|
|
|
362
396
|
|
|
363
397
|
for adapter in discovered.adapters:
|
|
364
398
|
adapter_config = {
|
|
365
|
-
"type": adapter.
|
|
399
|
+
"type": adapter.adapter_type,
|
|
366
400
|
"enabled": True
|
|
367
401
|
}
|
|
368
402
|
|
|
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
|
|
403
|
+
# Add adapter-specific configuration from discovered config
|
|
404
|
+
adapter_config.update(adapter.config)
|
|
405
|
+
|
|
406
|
+
# Ensure type is set correctly (remove 'adapter' key if present)
|
|
407
|
+
if 'adapter' in adapter_config:
|
|
408
|
+
del adapter_config['adapter']
|
|
409
|
+
|
|
410
|
+
# Use adapter type as the key name
|
|
411
|
+
adapter_name = adapter.adapter_type
|
|
412
|
+
config_data["adapters"][adapter_name] = adapter_config
|
|
391
413
|
|
|
392
414
|
# Set first discovered adapter as default
|
|
393
415
|
if config_data["default_adapter"] is None:
|
|
394
|
-
config_data["default_adapter"] = adapter.
|
|
416
|
+
config_data["default_adapter"] = adapter.adapter_type
|
|
395
417
|
|
|
396
418
|
logger.info(f"Discovered {len(config_data['adapters'])} adapter(s) from environment")
|
|
397
419
|
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.34
|
|
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>
|
|
@@ -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=NKKpxMKtYBzkS7DVcj0AKzeO_ZpeIyXvB1W2qzW1TS4,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=V4b6qD_eHG1K6QLBi3-y4JIqzqnJdSWaH6k96NPZiB8,31540
|
|
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=pzwjcXCbUFQCvTM5sN-dKZcnMruSImuYPcC7__DEYZg,58182
|
|
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=P6Y_i6xmcLb9xtD3VHkXfujIU6VlQlUoteDdS2d0C-w,20144
|
|
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.34.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
|
|
45
|
+
mcp_ticketer-0.1.34.dist-info/METADATA,sha256=kKvvgMiqCmqPceTqnhV-InFn1jkNcvZfM_-yoKHyzR4,13191
|
|
46
|
+
mcp_ticketer-0.1.34.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
mcp_ticketer-0.1.34.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
|
|
48
|
+
mcp_ticketer-0.1.34.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
|
|
49
|
+
mcp_ticketer-0.1.34.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|