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

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,313 @@
1
+ """Update checker for mcp-ticketer package.
2
+
3
+ This module provides functionality to check PyPI for new versions and notify users.
4
+ Uses the existing HTTP client infrastructure to avoid code duplication.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ import sys
10
+ from datetime import datetime, timedelta
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ # Try to import packaging, fall back to simple string comparison if unavailable
15
+ try:
16
+ from packaging.version import Version
17
+
18
+ HAS_PACKAGING = True
19
+ except ImportError:
20
+ HAS_PACKAGING = False
21
+
22
+ class Version:
23
+ """Fallback version comparison using simple string sorting.
24
+
25
+ This is a minimal fallback when packaging is not available.
26
+ Works correctly for most semantic versions (X.Y.Z format).
27
+ """
28
+
29
+ def __init__(self, version_string: str):
30
+ """Initialize with version string."""
31
+ self.version_string = version_string
32
+ # Parse into tuple of integers for proper comparison
33
+ try:
34
+ parts = version_string.split(".")
35
+ # Handle pre-release versions by splitting on non-digit chars
36
+ self.parts = []
37
+ for part in parts:
38
+ # Extract leading digits
39
+ digits = ""
40
+ for char in part:
41
+ if char.isdigit():
42
+ digits += char
43
+ else:
44
+ break
45
+ if digits:
46
+ self.parts.append(int(digits))
47
+ except (ValueError, AttributeError):
48
+ # Fallback to string comparison if parsing fails
49
+ self.parts = None
50
+
51
+ def __gt__(self, other: "Version") -> bool:
52
+ """Compare versions."""
53
+ if self.parts is not None and other.parts is not None:
54
+ # Proper numeric comparison
55
+ return self.parts > other.parts
56
+ # Fallback to string comparison
57
+ return self.version_string > other.version_string
58
+
59
+ def __eq__(self, other: object) -> bool:
60
+ """Check equality."""
61
+ if not isinstance(other, Version):
62
+ return False
63
+ if self.parts is not None and other.parts is not None:
64
+ return self.parts == other.parts
65
+ return self.version_string == other.version_string
66
+
67
+
68
+ from ..__version__ import __version__
69
+
70
+ logger = logging.getLogger(__name__)
71
+
72
+ # Cache configuration
73
+ CACHE_DIR = Path.home() / ".mcp-ticketer"
74
+ CACHE_FILE = CACHE_DIR / "update_check_cache.json"
75
+ CACHE_DURATION_HOURS = 24
76
+
77
+ # PyPI API configuration
78
+ PYPI_API_URL = "https://pypi.org/pypi/mcp-ticketer/json"
79
+ PYPI_PROJECT_URL = "https://pypi.org/project/mcp-ticketer/"
80
+
81
+
82
+ class UpdateInfo:
83
+ """Container for update information."""
84
+
85
+ def __init__(
86
+ self,
87
+ current_version: str,
88
+ latest_version: str,
89
+ needs_update: bool,
90
+ pypi_url: str,
91
+ release_date: str | None = None,
92
+ checked_at: str | None = None,
93
+ ):
94
+ """Initialize update information.
95
+
96
+ Args:
97
+ current_version: Currently installed version
98
+ latest_version: Latest version on PyPI
99
+ needs_update: Whether an update is available
100
+ pypi_url: URL to package on PyPI
101
+ release_date: Release date of latest version (ISO format)
102
+ checked_at: Timestamp of when check was performed (ISO format)
103
+
104
+ """
105
+ self.current_version = current_version
106
+ self.latest_version = latest_version
107
+ self.needs_update = needs_update
108
+ self.pypi_url = pypi_url
109
+ self.release_date = release_date
110
+ self.checked_at = checked_at or datetime.now().isoformat()
111
+
112
+ def to_dict(self) -> dict[str, Any]:
113
+ """Convert to dictionary."""
114
+ return {
115
+ "current_version": self.current_version,
116
+ "latest_version": self.latest_version,
117
+ "needs_update": self.needs_update,
118
+ "pypi_url": self.pypi_url,
119
+ "release_date": self.release_date,
120
+ "checked_at": self.checked_at,
121
+ }
122
+
123
+ @classmethod
124
+ def from_dict(cls, data: dict[str, Any]) -> "UpdateInfo":
125
+ """Create from dictionary."""
126
+ return cls(
127
+ current_version=data["current_version"],
128
+ latest_version=data["latest_version"],
129
+ needs_update=data["needs_update"],
130
+ pypi_url=data["pypi_url"],
131
+ release_date=data.get("release_date"),
132
+ checked_at=data.get("checked_at"),
133
+ )
134
+
135
+
136
+ def _ensure_cache_dir() -> None:
137
+ """Ensure cache directory exists."""
138
+ try:
139
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
140
+ except OSError as e:
141
+ logger.warning(f"Failed to create cache directory: {e}")
142
+
143
+
144
+ def _load_cache() -> dict[str, Any] | None:
145
+ """Load cached update information.
146
+
147
+ Returns:
148
+ Cached data or None if cache doesn't exist or is invalid
149
+
150
+ """
151
+ try:
152
+ if not CACHE_FILE.exists():
153
+ return None
154
+
155
+ with open(CACHE_FILE, encoding="utf-8") as f:
156
+ data = json.load(f)
157
+
158
+ # Validate cache structure
159
+ if not isinstance(data, dict) or "checked_at" not in data:
160
+ logger.debug("Invalid cache format, ignoring")
161
+ return None
162
+
163
+ return data
164
+ except (OSError, json.JSONDecodeError) as e:
165
+ logger.debug(f"Failed to load cache: {e}")
166
+ return None
167
+
168
+
169
+ def _save_cache(update_info: UpdateInfo) -> None:
170
+ """Save update information to cache.
171
+
172
+ Args:
173
+ update_info: Update information to cache
174
+
175
+ """
176
+ try:
177
+ _ensure_cache_dir()
178
+ with open(CACHE_FILE, "w", encoding="utf-8") as f:
179
+ json.dump(update_info.to_dict(), f, indent=2)
180
+ except OSError as e:
181
+ logger.warning(f"Failed to save cache: {e}")
182
+
183
+
184
+ def should_check_updates(force: bool = False) -> bool:
185
+ """Check if enough time has passed since last check.
186
+
187
+ Args:
188
+ force: If True, always return True (force check)
189
+
190
+ Returns:
191
+ True if check should be performed
192
+
193
+ """
194
+ if force:
195
+ return True
196
+
197
+ cache = _load_cache()
198
+ if not cache:
199
+ return True
200
+
201
+ try:
202
+ checked_at = datetime.fromisoformat(cache["checked_at"])
203
+ age = datetime.now() - checked_at
204
+ return age > timedelta(hours=CACHE_DURATION_HOURS)
205
+ except (ValueError, KeyError) as e:
206
+ logger.debug(f"Invalid cache timestamp: {e}")
207
+ return True
208
+
209
+
210
+ async def check_for_updates(force: bool = False) -> UpdateInfo:
211
+ """Check PyPI for latest version.
212
+
213
+ Args:
214
+ force: If True, bypass cache and force check
215
+
216
+ Returns:
217
+ UpdateInfo object with version information
218
+
219
+ Raises:
220
+ Exception: If PyPI API request fails
221
+
222
+ """
223
+ # Suppress httpx INFO logging to keep output clean
224
+ logging.getLogger("httpx").setLevel(logging.WARNING)
225
+
226
+ current_version = __version__
227
+
228
+ # Check cache first (unless forced)
229
+ if not force:
230
+ cache = _load_cache()
231
+ if cache and cache.get("current_version") == current_version:
232
+ # Return cached info if it's for the current version
233
+ return UpdateInfo.from_dict(cache)
234
+
235
+ # Fetch from PyPI - use httpx directly for simplicity
236
+ import httpx
237
+
238
+ async with httpx.AsyncClient(timeout=10.0) as client:
239
+ response = await client.get(PYPI_API_URL)
240
+ response.raise_for_status()
241
+ response_data = response.json()
242
+
243
+ # Extract version information
244
+ latest_version = response_data["info"]["version"]
245
+
246
+ # Get release date from releases data
247
+ releases = response_data.get("releases", {})
248
+ release_date = None
249
+ if latest_version in releases and releases[latest_version]:
250
+ # Get upload_time from first file in the release
251
+ upload_time = releases[latest_version][0].get("upload_time")
252
+ if upload_time:
253
+ # Convert to ISO format date only
254
+ release_date = upload_time.split("T")[0]
255
+
256
+ # Compare versions
257
+ needs_update = Version(latest_version) > Version(current_version)
258
+
259
+ # Create update info
260
+ update_info = UpdateInfo(
261
+ current_version=current_version,
262
+ latest_version=latest_version,
263
+ needs_update=needs_update,
264
+ pypi_url=PYPI_PROJECT_URL,
265
+ release_date=release_date,
266
+ )
267
+
268
+ # Cache the result
269
+ _save_cache(update_info)
270
+
271
+ return update_info
272
+
273
+
274
+ def detect_installation_method() -> str:
275
+ """Detect how mcp-ticketer was installed.
276
+
277
+ Returns:
278
+ Installation method: 'pipx', 'uv', or 'pip'
279
+
280
+ """
281
+ # Check for pipx
282
+ if "pipx" in sys.prefix or "pipx" in sys.executable:
283
+ return "pipx"
284
+
285
+ # Check for uv
286
+ if "uv" in sys.prefix or "uv" in sys.executable:
287
+ return "uv"
288
+ if ".venv" in sys.prefix and Path(sys.prefix).parent.name == ".venv":
289
+ # Common uv pattern
290
+ uv_bin = Path(sys.prefix).parent.parent / "uv"
291
+ if uv_bin.exists():
292
+ return "uv"
293
+
294
+ # Default to pip
295
+ return "pip"
296
+
297
+
298
+ def get_upgrade_command() -> str:
299
+ """Get the appropriate upgrade command for the installation method.
300
+
301
+ Returns:
302
+ Command string to upgrade mcp-ticketer
303
+
304
+ """
305
+ method = detect_installation_method()
306
+
307
+ commands = {
308
+ "pipx": "pipx upgrade mcp-ticketer",
309
+ "uv": "uv pip install --upgrade mcp-ticketer",
310
+ "pip": "pip install --upgrade mcp-ticketer",
311
+ }
312
+
313
+ return commands.get(method, "pip install --upgrade mcp-ticketer")
mcp_ticketer/cli/utils.py CHANGED
@@ -156,7 +156,7 @@ class CommonPatterns:
156
156
  @staticmethod
157
157
  def get_adapter(
158
158
  override_adapter: str | None = None, override_config: dict | None = None
159
- ):
159
+ ) -> Any:
160
160
  """Get configured adapter instance with environment variable support."""
161
161
  config = CommonPatterns.load_config()
162
162
 
@@ -321,33 +321,35 @@ class CommonPatterns:
321
321
  )
322
322
 
323
323
 
324
- def async_command(f: Callable[..., T]) -> Callable[..., T]:
325
- """Decorator to handle async CLI commands."""
324
+ def async_command(f: Callable[..., Any]) -> Callable[..., Any]:
325
+ """Handle async CLI commands via decorator."""
326
326
 
327
327
  @wraps(f)
328
- def wrapper(*args, **kwargs):
328
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
329
329
  return asyncio.run(f(*args, **kwargs))
330
330
 
331
331
  return wrapper
332
332
 
333
333
 
334
- def with_adapter(f: Callable) -> Callable:
335
- """Decorator to inject adapter instance into CLI commands."""
334
+ def with_adapter(f: Callable[..., Any]) -> Callable[..., Any]:
335
+ """Inject adapter instance into CLI commands via decorator."""
336
336
 
337
337
  @wraps(f)
338
- def wrapper(adapter: str | None = None, *args, **kwargs):
338
+ def wrapper(adapter: str | None = None, *args: Any, **kwargs: Any) -> Any:
339
339
  adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
340
340
  return f(adapter_instance, *args, **kwargs)
341
341
 
342
342
  return wrapper
343
343
 
344
344
 
345
- def with_progress(message: str = "Processing..."):
346
- """Decorator to show progress spinner for long-running operations."""
345
+ def with_progress(
346
+ message: str = "Processing...",
347
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
348
+ """Show progress spinner for long-running operations via decorator."""
347
349
 
348
- def decorator(f: Callable) -> Callable:
350
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
349
351
  @wraps(f)
350
- def wrapper(*args, **kwargs):
352
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
351
353
  with Progress(
352
354
  SpinnerColumn(),
353
355
  TextColumn("[progress.description]{task.description}"),
@@ -361,12 +363,14 @@ def with_progress(message: str = "Processing..."):
361
363
  return decorator
362
364
 
363
365
 
364
- def validate_required_fields(**field_map):
365
- """Decorator to validate required fields are provided."""
366
+ def validate_required_fields(
367
+ **field_map: str,
368
+ ) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
369
+ """Validate required fields are provided via decorator."""
366
370
 
367
- def decorator(f: Callable) -> Callable:
371
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
368
372
  @wraps(f)
369
- def wrapper(*args, **kwargs):
373
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
370
374
  missing_fields = []
371
375
  for field_name, display_name in field_map.items():
372
376
  if field_name in kwargs and kwargs[field_name] is None:
@@ -376,7 +380,7 @@ def validate_required_fields(**field_map):
376
380
  console.print(
377
381
  f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}"
378
382
  )
379
- raise typer.Exit(1)
383
+ raise typer.Exit(1) from None
380
384
 
381
385
  return f(*args, **kwargs)
382
386
 
@@ -385,24 +389,24 @@ def validate_required_fields(**field_map):
385
389
  return decorator
386
390
 
387
391
 
388
- def handle_adapter_errors(f: Callable) -> Callable:
389
- """Decorator to handle common adapter errors gracefully."""
392
+ def handle_adapter_errors(f: Callable[..., Any]) -> Callable[..., Any]:
393
+ """Handle common adapter errors gracefully via decorator."""
390
394
 
391
395
  @wraps(f)
392
- def wrapper(*args, **kwargs):
396
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
393
397
  try:
394
398
  return f(*args, **kwargs)
395
399
  except ConnectionError as e:
396
400
  console.print(f"[red]Connection Error:[/red] {e}")
397
401
  console.print("Check your network connection and adapter configuration")
398
- raise typer.Exit(1)
402
+ raise typer.Exit(1) from None
399
403
  except ValueError as e:
400
404
  console.print(f"[red]Configuration Error:[/red] {e}")
401
405
  console.print("Run 'mcp-ticketer init' to configure your adapter")
402
- raise typer.Exit(1)
406
+ raise typer.Exit(1) from None
403
407
  except Exception as e:
404
408
  console.print(f"[red]Unexpected Error:[/red] {e}")
405
- raise typer.Exit(1)
409
+ raise typer.Exit(1) from None
406
410
 
407
411
  return wrapper
408
412
 
@@ -468,39 +472,39 @@ class ConfigValidator:
468
472
  class CommandBuilder:
469
473
  """Builder for common CLI command patterns."""
470
474
 
471
- def __init__(self):
472
- self._validators = []
473
- self._handlers = []
474
- self._decorators = []
475
+ def __init__(self) -> None:
476
+ self._validators: list[Callable[..., Any]] = []
477
+ self._handlers: list[Callable[..., Any]] = []
478
+ self._decorators: list[Callable[..., Any]] = []
475
479
 
476
- def with_adapter_validation(self):
480
+ def with_adapter_validation(self) -> "CommandBuilder":
477
481
  """Add adapter configuration validation."""
478
482
  self._validators.append(self._validate_adapter)
479
483
  return self
480
484
 
481
- def with_async_support(self):
485
+ def with_async_support(self) -> "CommandBuilder":
482
486
  """Add async support to command."""
483
487
  self._decorators.append(async_command)
484
488
  return self
485
489
 
486
- def with_error_handling(self):
490
+ def with_error_handling(self) -> "CommandBuilder":
487
491
  """Add error handling decorator."""
488
492
  self._decorators.append(handle_adapter_errors)
489
493
  return self
490
494
 
491
- def with_progress(self, message: str = "Processing..."):
495
+ def with_progress(self, message: str = "Processing...") -> "CommandBuilder":
492
496
  """Add progress spinner."""
493
497
  self._decorators.append(with_progress(message))
494
498
  return self
495
499
 
496
- def build(self, func: Callable) -> Callable:
500
+ def build(self, func: Callable[..., Any]) -> Callable[..., Any]:
497
501
  """Build the decorated function."""
498
502
  decorated_func = func
499
503
  for decorator in reversed(self._decorators):
500
504
  decorated_func = decorator(decorated_func)
501
505
  return decorated_func
502
506
 
503
- def _validate_adapter(self, *args, **kwargs):
507
+ def _validate_adapter(self, *args: Any, **kwargs: Any) -> None:
504
508
  """Validate adapter configuration."""
505
509
  config = CommonPatterns.load_config()
506
510
  default_adapter = config.get("default_adapter", "aitrackdown")
@@ -514,10 +518,10 @@ class CommandBuilder:
514
518
  for issue in issues:
515
519
  console.print(f" • {issue}")
516
520
  console.print("Run 'mcp-ticketer init' to fix configuration")
517
- raise typer.Exit(1)
521
+ raise typer.Exit(1) from None
518
522
 
519
523
 
520
- def create_standard_ticket_command(operation: str):
524
+ def create_standard_ticket_command(operation: str) -> Callable[..., str]:
521
525
  """Create a standard ticket operation command."""
522
526
 
523
527
  def command_template(
@@ -529,7 +533,7 @@ def create_standard_ticket_command(operation: str):
529
533
  assignee: str | None = None,
530
534
  tags: list[str] | None = None,
531
535
  adapter: str | None = None,
532
- ):
536
+ ) -> str:
533
537
  """Template for ticket commands."""
534
538
  # Build ticket data
535
539
  ticket_data = {}
@@ -568,11 +572,11 @@ class TicketCommands:
568
572
  @async_command
569
573
  @handle_adapter_errors
570
574
  async def list_tickets(
571
- adapter_instance,
575
+ adapter_instance: Any,
572
576
  state: TicketState | None = None,
573
577
  priority: Priority | None = None,
574
578
  limit: int = 10,
575
- ):
579
+ ) -> None:
576
580
  """List tickets with filters."""
577
581
  filters = {}
578
582
  if state:
@@ -587,13 +591,13 @@ class TicketCommands:
587
591
  @async_command
588
592
  @handle_adapter_errors
589
593
  async def show_ticket(
590
- adapter_instance, ticket_id: str, show_comments: bool = False
591
- ):
594
+ adapter_instance: Any, ticket_id: str, show_comments: bool = False
595
+ ) -> None:
592
596
  """Show ticket details."""
593
597
  ticket = await adapter_instance.read(ticket_id)
594
598
  if not ticket:
595
599
  console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
596
- raise typer.Exit(1)
600
+ raise typer.Exit(1) from None
597
601
 
598
602
  comments = None
599
603
  if show_comments:
@@ -628,7 +632,7 @@ class TicketCommands:
628
632
  """Update a ticket."""
629
633
  if not updates:
630
634
  console.print("[yellow]No updates specified[/yellow]")
631
- raise typer.Exit(1)
635
+ raise typer.Exit(1) from None
632
636
 
633
637
  updates["ticket_id"] = ticket_id
634
638
  return CommonPatterns.queue_operation(updates, "update", adapter)
@@ -1,17 +1,51 @@
1
1
  """Core models and abstractions for MCP Ticketer."""
2
2
 
3
3
  from .adapter import BaseAdapter
4
- from .models import Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
4
+ from .instructions import (
5
+ InstructionsError,
6
+ InstructionsNotFoundError,
7
+ InstructionsValidationError,
8
+ TicketInstructionsManager,
9
+ get_instructions,
10
+ )
11
+ from .models import (
12
+ Attachment,
13
+ Comment,
14
+ Epic,
15
+ Priority,
16
+ ProjectUpdate,
17
+ ProjectUpdateHealth,
18
+ Task,
19
+ TicketState,
20
+ TicketType,
21
+ )
5
22
  from .registry import AdapterRegistry
23
+ from .state_matcher import (
24
+ SemanticStateMatcher,
25
+ StateMatchResult,
26
+ ValidationResult,
27
+ get_state_matcher,
28
+ )
6
29
 
7
30
  __all__ = [
8
31
  "Epic",
9
32
  "Task",
10
33
  "Comment",
11
34
  "Attachment",
35
+ "ProjectUpdate",
36
+ "ProjectUpdateHealth",
12
37
  "TicketState",
13
38
  "Priority",
14
39
  "TicketType",
15
40
  "BaseAdapter",
16
41
  "AdapterRegistry",
42
+ "TicketInstructionsManager",
43
+ "InstructionsError",
44
+ "InstructionsNotFoundError",
45
+ "InstructionsValidationError",
46
+ "get_instructions",
47
+ "SemanticStateMatcher",
48
+ "StateMatchResult",
49
+ "ValidationResult",
50
+ "get_state_matcher",
17
51
  ]