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.

@@ -1,6 +1,6 @@
1
1
  """Version information for mcp-ticketer package."""
2
2
 
3
- __version__ = "0.1.33"
3
+ __version__ = "0.1.36"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # Package metadata
@@ -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 safe_import_config():
19
- """Safely import configuration with fallback."""
20
- try:
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
-
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 queue manager with fallback."""
38
+ """Safely import worker manager with fallback."""
81
39
  try:
82
- from ..queue.manager import QueueManager as RealQueueManager
40
+ from ..queue.manager import WorkerManager as RealWorkerManager
83
41
 
84
- # Test if the real queue manager works
42
+ # Test if the real worker manager works
85
43
  try:
86
- qm = RealQueueManager()
44
+ wm = RealWorkerManager()
87
45
  # Test a basic operation
88
- qm.get_worker_status()
89
- return RealQueueManager
46
+ wm.get_status()
47
+ return RealWorkerManager
90
48
  except Exception:
91
- # Real queue manager failed, use fallback
49
+ # Real worker manager failed, use fallback
92
50
  pass
93
51
 
94
52
  except ImportError:
95
53
  pass
96
54
 
97
- class MockQueueManager:
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 MockQueueManager
68
+ return MockWorkerManager
108
69
 
109
70
  # Initialize with safe imports
110
- get_config = safe_import_config()
111
71
  AdapterRegistry = safe_import_registry()
112
- QueueManager = safe_import_queue_manager()
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
- # Check if this is a mock config
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"⚠️ Could not load configuration: {e}")
93
+ console.print(f"Could not load configuration: {e}")
94
+ raise e
139
95
 
140
96
  try:
141
- self.queue_manager = QueueManager()
142
- # Check if this is a mock queue manager
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.queue_manager = None
100
+ self.worker_manager = None
150
101
  self.queue_available = False
151
- console.print(f"⚠️ Could not initialize queue manager: {e}")
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
- adapters = self.config.get_enabled_adapters()
239
- config_status["adapters_configured"] = len(adapters)
240
- config_status["default_adapter"] = self.config.default_adapter
241
-
242
- if not adapters:
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(adapters)} adapter(s) configured")
202
+ console.print(f"✅ {len(adapters_config)} adapter(s) configured")
250
203
 
251
204
  # Check each adapter configuration
252
- for name, adapter_config in adapters.items():
205
+ for name, adapter_config in adapters_config.items():
253
206
  try:
254
- adapter_class = AdapterRegistry.get_adapter(adapter_config.type.value)
255
- adapter = adapter_class(adapter_config.dict())
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
- adapters = self.config.get_enabled_adapters()
299
- adapter_status["total_adapters"] = len(adapters)
300
-
301
- for name, adapter_config in adapters.items():
302
- # Handle both dict and object adapter configs
303
- if isinstance(adapter_config, dict):
304
- adapter_type = adapter_config.get("type", "unknown")
305
- config_dict = adapter_config
306
- else:
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
- # Import AdapterRegistry safely
319
- try:
320
- from ..core.registry import AdapterRegistry
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.queue_manager.get_worker_status()
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.queue_manager.get_queue_stats()
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 queue manager
483
- if hasattr(self.queue_manager, 'start_worker'):
484
- result = await self.queue_manager.start_worker()
485
- test_result["success"] = True
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
- if hasattr(self.queue_manager, 'queue_task'):
530
- queue_id = await self.queue_manager.queue_task("create", test_task, "aitrackdown")
531
- test_result["success"] = True
532
- test_result["details"] = f"Test task queued successfully: {queue_id}"
533
- else:
534
- test_result["error"] = "Queue manager doesn't support task queuing"
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
- config["adapters"][adapter_type] = discovered_adapter.config
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"] = {"base_path": base_path or ".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
- config = load_config()
931
- adapter_name = (
932
- adapter.value if adapter else config.get("default_adapter", "aitrackdown")
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 ONLY.
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. Default to aitrackdown adapter
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"}}
@@ -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 - try environment discovery
288
- logger.info("No configuration file found, attempting environment discovery")
289
- config_data = self._discover_from_environment()
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 EnvironmentDiscovery
356
+ from .env_discovery import EnvDiscovery
339
357
 
340
- discovery = EnvironmentDiscovery()
341
- discovered = discovery.discover_all()
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.type.value,
383
+ "type": adapter.adapter_type,
366
384
  "enabled": True
367
385
  }
368
386
 
369
- # Add adapter-specific configuration
370
- if adapter.type.value == "linear":
371
- adapter_config.update({
372
- "api_key": adapter.credentials.get("api_key"),
373
- "team_id": adapter.credentials.get("team_id"),
374
- "project_id": adapter.credentials.get("project_id")
375
- })
376
- elif adapter.type.value == "github":
377
- adapter_config.update({
378
- "token": adapter.credentials.get("token"),
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.name
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
- # Load files in reverse order (lowest priority first)
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 ID (recommended but not required)
286
- team_id = self._find_key_value(env_vars, LINEAR_TEAM_PATTERNS)
287
- if team_id:
288
- config["team_id"] = team_id
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 (recommended)")
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.33
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=YbtZf8fo9cJTUcdyWdoua2cQU5TJHm7vQ3mwfSr7-mI,1118
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=FaxlcaHAj3eep_h8Bzv8ktPCeZ5r7SgJG25QOqK3gm4,31095
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=TxhCdmEi97rrDkCd0nT4KYRtZSboTfPSrw7SL607goA,54970
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=2ptUrp2ELZsox0kSxAI5DFrHonOU999qh4MxbLv6VBQ,21155
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=A3K2JXn65qwpI80_J05oA-6DRjcONi6ZqemU02O4L8U,19120
28
- mcp_ticketer/core/env_discovery.py,sha256=wKp2Pi5vQMGOTrM1690IBv_eoABly-pD8ah7n1zSWDc,17710
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.33.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
45
- mcp_ticketer-0.1.33.dist-info/METADATA,sha256=gla674ca9CyqLOSch1ZYZMN_7l-6wnymrS0ODYrhkR4,13191
46
- mcp_ticketer-0.1.33.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- mcp_ticketer-0.1.33.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
48
- mcp_ticketer-0.1.33.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
49
- mcp_ticketer-0.1.33.dist-info/RECORD,,
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,,