mcp-ticketer 0.1.11__py3-none-any.whl → 0.1.13__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.

@@ -0,0 +1,606 @@
1
+ """Project-level configuration management with hierarchical resolution.
2
+
3
+ This module provides a comprehensive configuration system that supports:
4
+ - Project-specific configurations (.mcp-ticketer/config.json in project root)
5
+ - Global configurations (~/.mcp-ticketer/config.json)
6
+ - Environment variable overrides
7
+ - CLI flag overrides
8
+ - Hybrid mode for multi-platform synchronization
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import logging
14
+ from typing import Dict, Any, Optional, List
15
+ from pathlib import Path
16
+ from dataclasses import dataclass, field, asdict
17
+ from enum import Enum
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class AdapterType(str, Enum):
23
+ """Supported adapter types."""
24
+ AITRACKDOWN = "aitrackdown"
25
+ LINEAR = "linear"
26
+ JIRA = "jira"
27
+ GITHUB = "github"
28
+
29
+
30
+ class SyncStrategy(str, Enum):
31
+ """Hybrid mode synchronization strategies."""
32
+ PRIMARY_SOURCE = "primary_source" # One adapter is source of truth
33
+ BIDIRECTIONAL = "bidirectional" # Two-way sync between adapters
34
+ MIRROR = "mirror" # Clone tickets across all adapters
35
+
36
+
37
+ @dataclass
38
+ class AdapterConfig:
39
+ """Base configuration for a single adapter instance."""
40
+ adapter: str
41
+ enabled: bool = True
42
+
43
+ # Common fields (not all adapters use all fields)
44
+ api_key: Optional[str] = None
45
+ token: Optional[str] = None
46
+
47
+ # Linear-specific
48
+ team_id: Optional[str] = None
49
+ team_key: Optional[str] = None
50
+ workspace: Optional[str] = None
51
+
52
+ # JIRA-specific
53
+ server: Optional[str] = None
54
+ email: Optional[str] = None
55
+ api_token: Optional[str] = None
56
+ project_key: Optional[str] = None
57
+
58
+ # GitHub-specific
59
+ owner: Optional[str] = None
60
+ repo: Optional[str] = None
61
+
62
+ # AITrackdown-specific
63
+ base_path: Optional[str] = None
64
+
65
+ # Project ID (can be used by any adapter for scoping)
66
+ project_id: Optional[str] = None
67
+
68
+ # Additional adapter-specific configuration
69
+ additional_config: Dict[str, Any] = field(default_factory=dict)
70
+
71
+ def to_dict(self) -> Dict[str, Any]:
72
+ """Convert to dictionary, filtering None values."""
73
+ result = {}
74
+ for key, value in asdict(self).items():
75
+ if value is not None:
76
+ result[key] = value
77
+ return result
78
+
79
+ @classmethod
80
+ def from_dict(cls, data: Dict[str, Any]) -> 'AdapterConfig':
81
+ """Create from dictionary."""
82
+ # Extract known fields
83
+ known_fields = {
84
+ 'adapter', 'enabled', 'api_key', 'token', 'team_id', 'team_key',
85
+ 'workspace', 'server', 'email', 'api_token', 'project_key',
86
+ 'owner', 'repo', 'base_path', 'project_id'
87
+ }
88
+
89
+ kwargs = {}
90
+ additional = {}
91
+
92
+ for key, value in data.items():
93
+ if key in known_fields:
94
+ kwargs[key] = value
95
+ elif key != 'additional_config':
96
+ additional[key] = value
97
+
98
+ # Merge explicit additional_config
99
+ if 'additional_config' in data:
100
+ additional.update(data['additional_config'])
101
+
102
+ kwargs['additional_config'] = additional
103
+ return cls(**kwargs)
104
+
105
+
106
+ @dataclass
107
+ class ProjectConfig:
108
+ """Configuration for a specific project."""
109
+ adapter: str
110
+ api_key: Optional[str] = None
111
+ project_id: Optional[str] = None
112
+ team_id: Optional[str] = None
113
+ additional_config: Dict[str, Any] = field(default_factory=dict)
114
+
115
+ def to_dict(self) -> Dict[str, Any]:
116
+ """Convert to dictionary."""
117
+ return {k: v for k, v in asdict(self).items() if v is not None}
118
+
119
+ @classmethod
120
+ def from_dict(cls, data: Dict[str, Any]) -> 'ProjectConfig':
121
+ """Create from dictionary."""
122
+ return cls(**{k: v for k, v in data.items() if k in cls.__annotations__})
123
+
124
+
125
+ @dataclass
126
+ class HybridConfig:
127
+ """Configuration for hybrid mode (multi-adapter sync)."""
128
+ enabled: bool = False
129
+ adapters: List[str] = field(default_factory=list)
130
+ primary_adapter: Optional[str] = None
131
+ sync_strategy: SyncStrategy = SyncStrategy.PRIMARY_SOURCE
132
+
133
+ def to_dict(self) -> Dict[str, Any]:
134
+ """Convert to dictionary."""
135
+ result = asdict(self)
136
+ result['sync_strategy'] = self.sync_strategy.value
137
+ return result
138
+
139
+ @classmethod
140
+ def from_dict(cls, data: Dict[str, Any]) -> 'HybridConfig':
141
+ """Create from dictionary."""
142
+ data = data.copy()
143
+ if 'sync_strategy' in data:
144
+ data['sync_strategy'] = SyncStrategy(data['sync_strategy'])
145
+ return cls(**data)
146
+
147
+
148
+ @dataclass
149
+ class TicketerConfig:
150
+ """Complete ticketer configuration with hierarchical resolution."""
151
+ default_adapter: str = "aitrackdown"
152
+ project_configs: Dict[str, ProjectConfig] = field(default_factory=dict)
153
+ adapters: Dict[str, AdapterConfig] = field(default_factory=dict)
154
+ hybrid_mode: Optional[HybridConfig] = None
155
+
156
+ def to_dict(self) -> Dict[str, Any]:
157
+ """Convert to dictionary for JSON serialization."""
158
+ return {
159
+ "default_adapter": self.default_adapter,
160
+ "project_configs": {
161
+ path: config.to_dict()
162
+ for path, config in self.project_configs.items()
163
+ },
164
+ "adapters": {
165
+ name: config.to_dict()
166
+ for name, config in self.adapters.items()
167
+ },
168
+ "hybrid_mode": self.hybrid_mode.to_dict() if self.hybrid_mode else None
169
+ }
170
+
171
+ @classmethod
172
+ def from_dict(cls, data: Dict[str, Any]) -> 'TicketerConfig':
173
+ """Create from dictionary."""
174
+ # Parse project configs
175
+ project_configs = {}
176
+ if "project_configs" in data:
177
+ for path, config_data in data["project_configs"].items():
178
+ project_configs[path] = ProjectConfig.from_dict(config_data)
179
+
180
+ # Parse adapter configs
181
+ adapters = {}
182
+ if "adapters" in data:
183
+ for name, adapter_data in data["adapters"].items():
184
+ adapters[name] = AdapterConfig.from_dict(adapter_data)
185
+
186
+ # Parse hybrid config
187
+ hybrid_mode = None
188
+ if "hybrid_mode" in data and data["hybrid_mode"]:
189
+ hybrid_mode = HybridConfig.from_dict(data["hybrid_mode"])
190
+
191
+ return cls(
192
+ default_adapter=data.get("default_adapter", "aitrackdown"),
193
+ project_configs=project_configs,
194
+ adapters=adapters,
195
+ hybrid_mode=hybrid_mode
196
+ )
197
+
198
+
199
+ class ConfigValidator:
200
+ """Validate adapter configurations."""
201
+
202
+ @staticmethod
203
+ def validate_linear_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
204
+ """Validate Linear adapter configuration.
205
+
206
+ Returns:
207
+ Tuple of (is_valid, error_message)
208
+ """
209
+ required = ["api_key"]
210
+ for field in required:
211
+ if field not in config or not config[field]:
212
+ return False, f"Linear config missing required field: {field}"
213
+
214
+ # Warn if team_id is missing but don't fail
215
+ if not config.get("team_id"):
216
+ logger.warning("Linear config missing team_id - may be required for some operations")
217
+
218
+ return True, None
219
+
220
+ @staticmethod
221
+ def validate_github_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
222
+ """Validate GitHub adapter configuration.
223
+
224
+ Returns:
225
+ Tuple of (is_valid, error_message)
226
+ """
227
+ # token or api_key (aliases)
228
+ has_token = config.get("token") or config.get("api_key")
229
+ if not has_token:
230
+ return False, "GitHub config missing required field: token or api_key"
231
+
232
+ # project_id can be "owner/repo" format
233
+ if config.get("project_id"):
234
+ if "/" in config["project_id"]:
235
+ parts = config["project_id"].split("/")
236
+ if len(parts) == 2:
237
+ # Extract owner and repo from project_id
238
+ return True, None
239
+
240
+ # Otherwise need explicit owner and repo
241
+ required = ["owner", "repo"]
242
+ for field in required:
243
+ if field not in config or not config[field]:
244
+ return False, f"GitHub config missing required field: {field}"
245
+
246
+ return True, None
247
+
248
+ @staticmethod
249
+ def validate_jira_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
250
+ """Validate JIRA adapter configuration.
251
+
252
+ Returns:
253
+ Tuple of (is_valid, error_message)
254
+ """
255
+ required = ["server", "email", "api_token"]
256
+ for field in required:
257
+ if field not in config or not config[field]:
258
+ return False, f"JIRA config missing required field: {field}"
259
+
260
+ # Validate server URL format
261
+ server = config["server"]
262
+ if not server.startswith(("http://", "https://")):
263
+ return False, "JIRA server must be a valid URL (http:// or https://)"
264
+
265
+ return True, None
266
+
267
+ @staticmethod
268
+ def validate_aitrackdown_config(config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
269
+ """Validate AITrackdown adapter configuration.
270
+
271
+ Returns:
272
+ Tuple of (is_valid, error_message)
273
+ """
274
+ # AITrackdown has minimal requirements
275
+ # base_path is optional (defaults to .aitrackdown)
276
+ return True, None
277
+
278
+ @classmethod
279
+ def validate(cls, adapter_type: str, config: Dict[str, Any]) -> tuple[bool, Optional[str]]:
280
+ """Validate configuration for any adapter type.
281
+
282
+ Args:
283
+ adapter_type: Type of adapter
284
+ config: Configuration dictionary
285
+
286
+ Returns:
287
+ Tuple of (is_valid, error_message)
288
+ """
289
+ validators = {
290
+ AdapterType.LINEAR.value: cls.validate_linear_config,
291
+ AdapterType.GITHUB.value: cls.validate_github_config,
292
+ AdapterType.JIRA.value: cls.validate_jira_config,
293
+ AdapterType.AITRACKDOWN.value: cls.validate_aitrackdown_config,
294
+ }
295
+
296
+ validator = validators.get(adapter_type)
297
+ if not validator:
298
+ return False, f"Unknown adapter type: {adapter_type}"
299
+
300
+ return validator(config)
301
+
302
+
303
+ class ConfigResolver:
304
+ """Resolve configuration from multiple sources with hierarchical precedence.
305
+
306
+ Resolution order (highest to lowest priority):
307
+ 1. CLI overrides
308
+ 2. Environment variables
309
+ 3. Project-specific config (.mcp-ticketer/config.json)
310
+ 4. Auto-discovered .env files
311
+ 5. Global config (~/.mcp-ticketer/config.json)
312
+ """
313
+
314
+ # Global config location
315
+ GLOBAL_CONFIG_PATH = Path.home() / ".mcp-ticketer" / "config.json"
316
+
317
+ # Project config location (relative to project root)
318
+ PROJECT_CONFIG_SUBPATH = ".mcp-ticketer" / Path("config.json")
319
+
320
+ def __init__(self, project_path: Optional[Path] = None, enable_env_discovery: bool = True):
321
+ """Initialize config resolver.
322
+
323
+ Args:
324
+ project_path: Path to project root (defaults to cwd)
325
+ enable_env_discovery: Enable auto-discovery from .env files (default: True)
326
+ """
327
+ self.project_path = project_path or Path.cwd()
328
+ self.enable_env_discovery = enable_env_discovery
329
+ self._global_config: Optional[TicketerConfig] = None
330
+ self._project_config: Optional[TicketerConfig] = None
331
+ self._discovered_config: Optional['DiscoveryResult'] = None
332
+
333
+ def load_global_config(self) -> TicketerConfig:
334
+ """Load global configuration from ~/.mcp-ticketer/config.json."""
335
+ if self.GLOBAL_CONFIG_PATH.exists():
336
+ try:
337
+ with open(self.GLOBAL_CONFIG_PATH, 'r') as f:
338
+ data = json.load(f)
339
+ return TicketerConfig.from_dict(data)
340
+ except Exception as e:
341
+ logger.error(f"Failed to load global config: {e}")
342
+
343
+ # Return default config
344
+ return TicketerConfig()
345
+
346
+ def load_project_config(self, project_path: Optional[Path] = None) -> Optional[TicketerConfig]:
347
+ """Load project-specific configuration.
348
+
349
+ Args:
350
+ project_path: Path to project root (defaults to self.project_path)
351
+
352
+ Returns:
353
+ Project config if exists, None otherwise
354
+ """
355
+ proj_path = project_path or self.project_path
356
+ config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
357
+
358
+ if config_path.exists():
359
+ try:
360
+ with open(config_path, 'r') as f:
361
+ data = json.load(f)
362
+ return TicketerConfig.from_dict(data)
363
+ except Exception as e:
364
+ logger.error(f"Failed to load project config from {config_path}: {e}")
365
+
366
+ return None
367
+
368
+ def save_global_config(self, config: TicketerConfig) -> None:
369
+ """Save global configuration.
370
+
371
+ Args:
372
+ config: Configuration to save
373
+ """
374
+ self.GLOBAL_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
375
+ with open(self.GLOBAL_CONFIG_PATH, 'w') as f:
376
+ json.dump(config.to_dict(), f, indent=2)
377
+ logger.info(f"Saved global config to {self.GLOBAL_CONFIG_PATH}")
378
+
379
+ def save_project_config(self, config: TicketerConfig, project_path: Optional[Path] = None) -> None:
380
+ """Save project-specific configuration.
381
+
382
+ Args:
383
+ config: Configuration to save
384
+ project_path: Path to project root (defaults to self.project_path)
385
+ """
386
+ proj_path = project_path or self.project_path
387
+ config_path = proj_path / self.PROJECT_CONFIG_SUBPATH
388
+
389
+ config_path.parent.mkdir(parents=True, exist_ok=True)
390
+ with open(config_path, 'w') as f:
391
+ json.dump(config.to_dict(), f, indent=2)
392
+ logger.info(f"Saved project config to {config_path}")
393
+
394
+ def get_discovered_config(self) -> Optional['DiscoveryResult']:
395
+ """Get auto-discovered configuration from .env files.
396
+
397
+ Returns:
398
+ DiscoveryResult if env discovery is enabled, None otherwise
399
+ """
400
+ if not self.enable_env_discovery:
401
+ return None
402
+
403
+ if self._discovered_config is None:
404
+ # Import here to avoid circular dependency
405
+ from .env_discovery import discover_config
406
+ self._discovered_config = discover_config(self.project_path)
407
+
408
+ return self._discovered_config
409
+
410
+ def resolve_adapter_config(
411
+ self,
412
+ adapter_name: Optional[str] = None,
413
+ cli_overrides: Optional[Dict[str, Any]] = None
414
+ ) -> Dict[str, Any]:
415
+ """Resolve adapter configuration with hierarchical precedence.
416
+
417
+ Precedence (highest to lowest):
418
+ 1. CLI overrides
419
+ 2. Environment variables (os.getenv)
420
+ 3. Project-specific config (.mcp-ticketer/config.json)
421
+ 4. Auto-discovered .env files
422
+ 5. Global config (~/.mcp-ticketer/config.json)
423
+
424
+ Args:
425
+ adapter_name: Name of adapter to configure (defaults to default_adapter)
426
+ cli_overrides: CLI flag overrides
427
+
428
+ Returns:
429
+ Resolved configuration dictionary
430
+ """
431
+ # Load configs
432
+ global_config = self.load_global_config()
433
+ project_config = self.load_project_config()
434
+
435
+ # Determine which adapter to use
436
+ if adapter_name:
437
+ target_adapter = adapter_name
438
+ elif project_config and project_config.default_adapter:
439
+ target_adapter = project_config.default_adapter
440
+ else:
441
+ # Try to infer from discovered config
442
+ discovered = self.get_discovered_config()
443
+ if discovered:
444
+ primary = discovered.get_primary_adapter()
445
+ if primary:
446
+ target_adapter = primary.adapter_type
447
+ else:
448
+ target_adapter = global_config.default_adapter
449
+ else:
450
+ target_adapter = global_config.default_adapter
451
+
452
+ # Start with empty config
453
+ resolved_config = {"adapter": target_adapter}
454
+
455
+ # 1. Apply global adapter config
456
+ if target_adapter in global_config.adapters:
457
+ global_adapter_config = global_config.adapters[target_adapter].to_dict()
458
+ resolved_config.update(global_adapter_config)
459
+
460
+ # 2. Apply auto-discovered .env config (if enabled)
461
+ if self.enable_env_discovery:
462
+ discovered = self.get_discovered_config()
463
+ if discovered:
464
+ discovered_adapter = discovered.get_adapter_by_type(target_adapter)
465
+ if discovered_adapter:
466
+ # Merge discovered config
467
+ discovered_dict = {
468
+ k: v for k, v in discovered_adapter.config.items()
469
+ if k != "adapter" # Don't override adapter type
470
+ }
471
+ resolved_config.update(discovered_dict)
472
+ logger.debug(
473
+ f"Applied auto-discovered config from {discovered_adapter.found_in}"
474
+ )
475
+
476
+ # 3. Apply project-specific config if exists
477
+ if project_config:
478
+ # Check if this project has specific adapter config
479
+ project_path_str = str(self.project_path)
480
+ if project_path_str in project_config.project_configs:
481
+ proj_adapter_config = project_config.project_configs[project_path_str].to_dict()
482
+ resolved_config.update(proj_adapter_config)
483
+
484
+ # Also check if project has adapter-level overrides
485
+ if target_adapter in project_config.adapters:
486
+ proj_global_adapter_config = project_config.adapters[target_adapter].to_dict()
487
+ resolved_config.update(proj_global_adapter_config)
488
+
489
+ # 4. Apply environment variable overrides (os.getenv)
490
+ env_overrides = self._get_env_overrides(target_adapter)
491
+ resolved_config.update(env_overrides)
492
+
493
+ # 5. Apply CLI overrides
494
+ if cli_overrides:
495
+ resolved_config.update(cli_overrides)
496
+
497
+ return resolved_config
498
+
499
+ def _get_env_overrides(self, adapter_type: str) -> Dict[str, Any]:
500
+ """Get configuration overrides from environment variables.
501
+
502
+ Args:
503
+ adapter_type: Type of adapter
504
+
505
+ Returns:
506
+ Dictionary of overrides from environment
507
+ """
508
+ overrides = {}
509
+
510
+ # Override adapter type
511
+ if os.getenv("MCP_TICKETER_ADAPTER"):
512
+ overrides["adapter"] = os.getenv("MCP_TICKETER_ADAPTER")
513
+
514
+ # Common overrides
515
+ if os.getenv("MCP_TICKETER_API_KEY"):
516
+ overrides["api_key"] = os.getenv("MCP_TICKETER_API_KEY")
517
+
518
+ # Adapter-specific overrides
519
+ if adapter_type == AdapterType.LINEAR.value:
520
+ if os.getenv("MCP_TICKETER_LINEAR_API_KEY"):
521
+ overrides["api_key"] = os.getenv("MCP_TICKETER_LINEAR_API_KEY")
522
+ if os.getenv("MCP_TICKETER_LINEAR_TEAM_ID"):
523
+ overrides["team_id"] = os.getenv("MCP_TICKETER_LINEAR_TEAM_ID")
524
+ if os.getenv("LINEAR_API_KEY"):
525
+ overrides["api_key"] = os.getenv("LINEAR_API_KEY")
526
+
527
+ elif adapter_type == AdapterType.GITHUB.value:
528
+ if os.getenv("MCP_TICKETER_GITHUB_TOKEN"):
529
+ overrides["token"] = os.getenv("MCP_TICKETER_GITHUB_TOKEN")
530
+ if os.getenv("GITHUB_TOKEN"):
531
+ overrides["token"] = os.getenv("GITHUB_TOKEN")
532
+ if os.getenv("MCP_TICKETER_GITHUB_OWNER"):
533
+ overrides["owner"] = os.getenv("MCP_TICKETER_GITHUB_OWNER")
534
+ if os.getenv("MCP_TICKETER_GITHUB_REPO"):
535
+ overrides["repo"] = os.getenv("MCP_TICKETER_GITHUB_REPO")
536
+
537
+ elif adapter_type == AdapterType.JIRA.value:
538
+ if os.getenv("MCP_TICKETER_JIRA_SERVER"):
539
+ overrides["server"] = os.getenv("MCP_TICKETER_JIRA_SERVER")
540
+ if os.getenv("MCP_TICKETER_JIRA_EMAIL"):
541
+ overrides["email"] = os.getenv("MCP_TICKETER_JIRA_EMAIL")
542
+ if os.getenv("MCP_TICKETER_JIRA_TOKEN"):
543
+ overrides["api_token"] = os.getenv("MCP_TICKETER_JIRA_TOKEN")
544
+ if os.getenv("JIRA_SERVER"):
545
+ overrides["server"] = os.getenv("JIRA_SERVER")
546
+ if os.getenv("JIRA_EMAIL"):
547
+ overrides["email"] = os.getenv("JIRA_EMAIL")
548
+ if os.getenv("JIRA_API_TOKEN"):
549
+ overrides["api_token"] = os.getenv("JIRA_API_TOKEN")
550
+
551
+ elif adapter_type == AdapterType.AITRACKDOWN.value:
552
+ if os.getenv("MCP_TICKETER_AITRACKDOWN_BASE_PATH"):
553
+ overrides["base_path"] = os.getenv("MCP_TICKETER_AITRACKDOWN_BASE_PATH")
554
+
555
+ # Hybrid mode
556
+ if os.getenv("MCP_TICKETER_HYBRID_MODE"):
557
+ overrides["hybrid_mode_enabled"] = os.getenv("MCP_TICKETER_HYBRID_MODE").lower() == "true"
558
+ if os.getenv("MCP_TICKETER_HYBRID_ADAPTERS"):
559
+ overrides["hybrid_adapters"] = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS").split(",")
560
+
561
+ return overrides
562
+
563
+ def get_hybrid_config(self) -> Optional[HybridConfig]:
564
+ """Get hybrid mode configuration if enabled.
565
+
566
+ Returns:
567
+ HybridConfig if hybrid mode is enabled, None otherwise
568
+ """
569
+ # Check environment first
570
+ if os.getenv("MCP_TICKETER_HYBRID_MODE", "").lower() == "true":
571
+ adapters = os.getenv("MCP_TICKETER_HYBRID_ADAPTERS", "").split(",")
572
+ return HybridConfig(
573
+ enabled=True,
574
+ adapters=[a.strip() for a in adapters if a.strip()]
575
+ )
576
+
577
+ # Check project config
578
+ project_config = self.load_project_config()
579
+ if project_config and project_config.hybrid_mode and project_config.hybrid_mode.enabled:
580
+ return project_config.hybrid_mode
581
+
582
+ # Check global config
583
+ global_config = self.load_global_config()
584
+ if global_config.hybrid_mode and global_config.hybrid_mode.enabled:
585
+ return global_config.hybrid_mode
586
+
587
+ return None
588
+
589
+
590
+ # Singleton instance for global access
591
+ _default_resolver: Optional[ConfigResolver] = None
592
+
593
+
594
+ def get_config_resolver(project_path: Optional[Path] = None) -> ConfigResolver:
595
+ """Get the global config resolver instance.
596
+
597
+ Args:
598
+ project_path: Path to project root (defaults to cwd)
599
+
600
+ Returns:
601
+ ConfigResolver instance
602
+ """
603
+ global _default_resolver
604
+ if _default_resolver is None or project_path is not None:
605
+ _default_resolver = ConfigResolver(project_path)
606
+ return _default_resolver
@@ -173,7 +173,10 @@ class Queue:
173
173
  ''', (QueueStatus.PROCESSING.value, row[0]))
174
174
  conn.commit()
175
175
 
176
- return QueueItem.from_row(row)
176
+ # Create QueueItem from row and update status
177
+ item = QueueItem.from_row(row)
178
+ item.status = QueueStatus.PROCESSING
179
+ return item
177
180
 
178
181
  return None
179
182
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-ticketer
3
- Version: 0.1.11
3
+ Version: 0.1.13
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,35 +1,41 @@
1
1
  mcp_ticketer/__init__.py,sha256=ayPQdFr6msypD06_G96a1H0bdFCT1m1wDtv8MZBpY4I,496
2
- mcp_ticketer/__version__.py,sha256=Ym9ng_TEiLSfqVAhPCtwjrIqiM8S9cAUurRI6mpYEyg,1115
2
+ mcp_ticketer/__version__.py,sha256=_43DgubrWUqhaLrhYbn9KefWfUiLWjH_hMHmCrgw1Tc,1115
3
3
  mcp_ticketer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- mcp_ticketer/adapters/__init__.py,sha256=_QRLaX38EUsL-kMvJITY0lYHvrq_ip9Qw4Q1YLavJSo,283
5
- mcp_ticketer/adapters/aitrackdown.py,sha256=ICNimTtF6rPajuVoVEpmdw2TfjYjnWvao8prUwukNn0,15210
4
+ mcp_ticketer/adapters/__init__.py,sha256=K_1egvhHb5F_7yFceUx2YzPGEoc7vX-q8dMVaS4K6gw,356
5
+ mcp_ticketer/adapters/aitrackdown.py,sha256=gqS_N6VGLoG5itUu17ANG5SefaAITYoW-t2xL9SrY-Y,15372
6
6
  mcp_ticketer/adapters/github.py,sha256=onT8NhYaf9fIw2eCOTbZSkk7q4IoM7ZADRvRl9qrUz8,43850
7
+ mcp_ticketer/adapters/hybrid.py,sha256=H9B-pfWmDKXO3GgzxB8undEcZTMzLz_1a6zWhj7xfR0,18556
7
8
  mcp_ticketer/adapters/jira.py,sha256=rd-8PseTsRyQNPjsrUJ8vJ8vfBpa6HWFBieOUyvw0Tg,28954
8
- mcp_ticketer/adapters/linear.py,sha256=neTxVy-QD23tTI7XKtnc5CBCpm3yVCULlgxG5oFSQI4,51752
9
+ mcp_ticketer/adapters/linear.py,sha256=BUK40EF4yNVJV5ldJPFx7Ne4FXHYbrGCUAaFy2gWC9Y,65211
9
10
  mcp_ticketer/cache/__init__.py,sha256=MSi3GLXancfP2-edPC9TFAJk7r0j6H5-XmpMHnkGPbI,137
10
11
  mcp_ticketer/cache/memory.py,sha256=gTzv-xF7qGfiYVUjG7lnzo0ZcqgXQajMl4NAYUcaytg,5133
11
12
  mcp_ticketer/cli/__init__.py,sha256=YeljyLtv906TqkvRuEPhmKO-Uk0CberQ9I6kx1tx2UA,88
12
- mcp_ticketer/cli/main.py,sha256=adz3YXGnw7GY1h9jYgmIfYizf-z4ddhx2adFjncpZ3A,28294
13
+ mcp_ticketer/cli/configure.py,sha256=etFutvc0QpaVDMOsZiiN7wKuaT98Od1Tj9W6lsEWw5A,16351
14
+ mcp_ticketer/cli/discover.py,sha256=putWrGcctUH8K0fOMtr9MZA9VnWoXzbtoe7mXSkDxhg,13156
15
+ mcp_ticketer/cli/main.py,sha256=3NAHRr5Q4nCExbyQ6YIiUxn1nyqi_e4TBgR8OuMhHqk,30461
16
+ mcp_ticketer/cli/migrate_config.py,sha256=iZIstnlr9vkhiW_MlnSyJOkMi4KHQqrZ6Hz1ECD_VUk,6045
13
17
  mcp_ticketer/cli/queue_commands.py,sha256=f3pEHKZ43dBHEIoCBvdfvjfMB9_WJltps9ATwTzorY0,8160
14
18
  mcp_ticketer/cli/utils.py,sha256=NxsS91vKA8xZnDXKU2y0Gcyc4I_ctRyJj-wT7Xd1Q_Q,18589
15
- mcp_ticketer/core/__init__.py,sha256=NA-rDvwuAOZ9sUZVYJOWp8bR6mOBG8w_5lpWTT75JNI,318
16
- mcp_ticketer/core/adapter.py,sha256=Hh4G9Ri1afchfpRSX8z971roXf50kmyyDD98zbRxSEk,5442
19
+ mcp_ticketer/core/__init__.py,sha256=qpCZveQMyqU2JvYG9MG_c6X35z_VoSmjWdXGcUZqqmA,348
20
+ mcp_ticketer/core/adapter.py,sha256=6KfhceINHfDjs--RyA_rOLeM2phVD_D85S9D6s2lcuU,10075
17
21
  mcp_ticketer/core/config.py,sha256=9a2bksbcFr7KXeHSPY6KoSP5Pzt54utYPCmbM-1QKmk,13932
22
+ mcp_ticketer/core/env_discovery.py,sha256=SPoyq_y5j-3gJG5gYNVjCIIrbdzimOdDbTYySmQWZOA,17536
18
23
  mcp_ticketer/core/http_client.py,sha256=RM9CEMNcuRb-FxhAijmM_FeBMgxgh1OII9HIPBdJue0,13855
19
24
  mcp_ticketer/core/mappers.py,sha256=8I4jcqDqoQEdWlteDMpVeVF3Wo0fDCkmFPRr8oNv8gA,16933
20
- mcp_ticketer/core/models.py,sha256=K-bLuU_DNNVgjHnVFzAIbSa0kJwT2I3Hj24sCklwIYo,4374
25
+ mcp_ticketer/core/models.py,sha256=GhuTitY6t_QlqfEUvWT6Q2zvY7qAtx_SQyCMMn8iYkk,6473
26
+ mcp_ticketer/core/project_config.py,sha256=UqJpxAunmCvR9R_ev9ZrsLvG4engQe1OFQBEmcRx9cs,22254
21
27
  mcp_ticketer/core/registry.py,sha256=fwje0fnjp0YKPZ0SrVWk82SMNLs7CD0JlHQmx7SigNo,3537
22
28
  mcp_ticketer/mcp/__init__.py,sha256=Bvzof9vBu6VwcXcIZK8RgKv6ycRV9tDlO-9TUmd8zqQ,122
23
29
  mcp_ticketer/mcp/server.py,sha256=TDuU8ChZC2QYSRo0uGHkVReblTf--hriOjxo-pSAF_Y,34068
24
30
  mcp_ticketer/queue/__init__.py,sha256=xHBoUwor8ZdO8bIHc7nP25EsAp5Si5Co4g_8ybb7fes,230
25
31
  mcp_ticketer/queue/__main__.py,sha256=kQd6iOCKbbFqpRdbIRavuI4_G7-oE898JE4a0yLEYPE,108
26
32
  mcp_ticketer/queue/manager.py,sha256=79AH9oUxdBXH3lmJ3kIlFf2GQkWHL6XB6u5JqVWPq60,7571
27
- mcp_ticketer/queue/queue.py,sha256=UgbIChWPiyI7BJNQ9DYA92D2jVMMtmVWBzotI5ML51A,11394
33
+ mcp_ticketer/queue/queue.py,sha256=z4aivQCtsH5_OUr2OfXSfnFKzugTahNnwHw0LS3ZhZc,11549
28
34
  mcp_ticketer/queue/run_worker.py,sha256=HFoykfDpOoz8OUxWbQ2Fka_UlGrYwjPVZ-DEimGFH9o,802
29
35
  mcp_ticketer/queue/worker.py,sha256=cVjHR_kfnGKAkiUg0HuXCnbKeKNBBEuj0XZHgIuIn4k,14017
30
- mcp_ticketer-0.1.11.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
31
- mcp_ticketer-0.1.11.dist-info/METADATA,sha256=GjLKPEBTziutLdEpztLyQkGvxMCpiHArYsdSKltJwwA,11211
32
- mcp_ticketer-0.1.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- mcp_ticketer-0.1.11.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
34
- mcp_ticketer-0.1.11.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
35
- mcp_ticketer-0.1.11.dist-info/RECORD,,
36
+ mcp_ticketer-0.1.13.dist-info/licenses/LICENSE,sha256=KOVrunjtILSzY-2N8Lqa3-Q8dMaZIG4LrlLTr9UqL08,1073
37
+ mcp_ticketer-0.1.13.dist-info/METADATA,sha256=29lPCGuZI3U_pdIoRYH3C5h4Xn3DaZo-AfKDXQX7IYs,11211
38
+ mcp_ticketer-0.1.13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
39
+ mcp_ticketer-0.1.13.dist-info/entry_points.txt,sha256=o1IxVhnHnBNG7FZzbFq-Whcs1Djbofs0qMjiUYBLx2s,60
40
+ mcp_ticketer-0.1.13.dist-info/top_level.txt,sha256=WnAG4SOT1Vm9tIwl70AbGG_nA217YyV3aWFhxLH2rxw,13
41
+ mcp_ticketer-0.1.13.dist-info/RECORD,,