mcp-ticketer 0.3.5__py3-none-any.whl → 0.12.0__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 (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.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
@@ -4,9 +4,10 @@ import asyncio
4
4
  import json
5
5
  import logging
6
6
  import os
7
+ from collections.abc import Callable
7
8
  from functools import wraps
8
9
  from pathlib import Path
9
- from typing import Any, Callable, Optional, TypeVar
10
+ from typing import Any, TypeVar
10
11
 
11
12
  import typer
12
13
  from rich.console import Console
@@ -154,8 +155,8 @@ class CommonPatterns:
154
155
 
155
156
  @staticmethod
156
157
  def get_adapter(
157
- override_adapter: Optional[str] = None, override_config: Optional[dict] = None
158
- ):
158
+ override_adapter: str | None = None, override_config: dict | None = None
159
+ ) -> Any:
159
160
  """Get configured adapter instance with environment variable support."""
160
161
  config = CommonPatterns.load_config()
161
162
 
@@ -206,7 +207,7 @@ class CommonPatterns:
206
207
  def queue_operation(
207
208
  ticket_data: dict[str, Any],
208
209
  operation: str,
209
- adapter_name: Optional[str] = None,
210
+ adapter_name: str | None = None,
210
211
  show_progress: bool = True,
211
212
  ) -> str:
212
213
  """Queue an operation and optionally start the worker."""
@@ -265,7 +266,7 @@ class CommonPatterns:
265
266
  console.print(table)
266
267
 
267
268
  @staticmethod
268
- def display_ticket_details(ticket: Task, comments: Optional[list] = None) -> None:
269
+ def display_ticket_details(ticket: Task, comments: list | None = None) -> None:
269
270
  """Display detailed ticket information."""
270
271
  console.print(f"\n[bold]Ticket: {ticket.id}[/bold]")
271
272
  console.print(f"Title: {ticket.title}")
@@ -320,33 +321,35 @@ class CommonPatterns:
320
321
  )
321
322
 
322
323
 
323
- def async_command(f: Callable[..., T]) -> Callable[..., T]:
324
- """Decorator to handle async CLI commands."""
324
+ def async_command(f: Callable[..., Any]) -> Callable[..., Any]:
325
+ """Handle async CLI commands via decorator."""
325
326
 
326
327
  @wraps(f)
327
- def wrapper(*args, **kwargs):
328
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
328
329
  return asyncio.run(f(*args, **kwargs))
329
330
 
330
331
  return wrapper
331
332
 
332
333
 
333
- def with_adapter(f: Callable) -> Callable:
334
- """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."""
335
336
 
336
337
  @wraps(f)
337
- def wrapper(adapter: Optional[str] = None, *args, **kwargs):
338
+ def wrapper(adapter: str | None = None, *args: Any, **kwargs: Any) -> Any:
338
339
  adapter_instance = CommonPatterns.get_adapter(override_adapter=adapter)
339
340
  return f(adapter_instance, *args, **kwargs)
340
341
 
341
342
  return wrapper
342
343
 
343
344
 
344
- def with_progress(message: str = "Processing..."):
345
- """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."""
346
349
 
347
- def decorator(f: Callable) -> Callable:
350
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
348
351
  @wraps(f)
349
- def wrapper(*args, **kwargs):
352
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
350
353
  with Progress(
351
354
  SpinnerColumn(),
352
355
  TextColumn("[progress.description]{task.description}"),
@@ -360,12 +363,14 @@ def with_progress(message: str = "Processing..."):
360
363
  return decorator
361
364
 
362
365
 
363
- def validate_required_fields(**field_map):
364
- """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."""
365
370
 
366
- def decorator(f: Callable) -> Callable:
371
+ def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
367
372
  @wraps(f)
368
- def wrapper(*args, **kwargs):
373
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
369
374
  missing_fields = []
370
375
  for field_name, display_name in field_map.items():
371
376
  if field_name in kwargs and kwargs[field_name] is None:
@@ -375,7 +380,7 @@ def validate_required_fields(**field_map):
375
380
  console.print(
376
381
  f"[red]Error:[/red] Missing required fields: {', '.join(missing_fields)}"
377
382
  )
378
- raise typer.Exit(1)
383
+ raise typer.Exit(1) from None
379
384
 
380
385
  return f(*args, **kwargs)
381
386
 
@@ -384,24 +389,24 @@ def validate_required_fields(**field_map):
384
389
  return decorator
385
390
 
386
391
 
387
- def handle_adapter_errors(f: Callable) -> Callable:
388
- """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."""
389
394
 
390
395
  @wraps(f)
391
- def wrapper(*args, **kwargs):
396
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
392
397
  try:
393
398
  return f(*args, **kwargs)
394
399
  except ConnectionError as e:
395
400
  console.print(f"[red]Connection Error:[/red] {e}")
396
401
  console.print("Check your network connection and adapter configuration")
397
- raise typer.Exit(1)
402
+ raise typer.Exit(1) from None
398
403
  except ValueError as e:
399
404
  console.print(f"[red]Configuration Error:[/red] {e}")
400
405
  console.print("Run 'mcp-ticketer init' to configure your adapter")
401
- raise typer.Exit(1)
406
+ raise typer.Exit(1) from None
402
407
  except Exception as e:
403
408
  console.print(f"[red]Unexpected Error:[/red] {e}")
404
- raise typer.Exit(1)
409
+ raise typer.Exit(1) from None
405
410
 
406
411
  return wrapper
407
412
 
@@ -446,7 +451,7 @@ class ConfigValidator:
446
451
  return issues
447
452
 
448
453
  @staticmethod
449
- def _get_env_var(adapter_type: str, field: str) -> Optional[str]:
454
+ def _get_env_var(adapter_type: str, field: str) -> str | None:
450
455
  """Get corresponding environment variable name for a config field."""
451
456
  env_mapping = {
452
457
  "github": {
@@ -467,39 +472,39 @@ class ConfigValidator:
467
472
  class CommandBuilder:
468
473
  """Builder for common CLI command patterns."""
469
474
 
470
- def __init__(self):
471
- self._validators = []
472
- self._handlers = []
473
- 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]] = []
474
479
 
475
- def with_adapter_validation(self):
480
+ def with_adapter_validation(self) -> "CommandBuilder":
476
481
  """Add adapter configuration validation."""
477
482
  self._validators.append(self._validate_adapter)
478
483
  return self
479
484
 
480
- def with_async_support(self):
485
+ def with_async_support(self) -> "CommandBuilder":
481
486
  """Add async support to command."""
482
487
  self._decorators.append(async_command)
483
488
  return self
484
489
 
485
- def with_error_handling(self):
490
+ def with_error_handling(self) -> "CommandBuilder":
486
491
  """Add error handling decorator."""
487
492
  self._decorators.append(handle_adapter_errors)
488
493
  return self
489
494
 
490
- def with_progress(self, message: str = "Processing..."):
495
+ def with_progress(self, message: str = "Processing...") -> "CommandBuilder":
491
496
  """Add progress spinner."""
492
497
  self._decorators.append(with_progress(message))
493
498
  return self
494
499
 
495
- def build(self, func: Callable) -> Callable:
500
+ def build(self, func: Callable[..., Any]) -> Callable[..., Any]:
496
501
  """Build the decorated function."""
497
502
  decorated_func = func
498
503
  for decorator in reversed(self._decorators):
499
504
  decorated_func = decorator(decorated_func)
500
505
  return decorated_func
501
506
 
502
- def _validate_adapter(self, *args, **kwargs):
507
+ def _validate_adapter(self, *args: Any, **kwargs: Any) -> None:
503
508
  """Validate adapter configuration."""
504
509
  config = CommonPatterns.load_config()
505
510
  default_adapter = config.get("default_adapter", "aitrackdown")
@@ -513,22 +518,22 @@ class CommandBuilder:
513
518
  for issue in issues:
514
519
  console.print(f" • {issue}")
515
520
  console.print("Run 'mcp-ticketer init' to fix configuration")
516
- raise typer.Exit(1)
521
+ raise typer.Exit(1) from None
517
522
 
518
523
 
519
- def create_standard_ticket_command(operation: str):
524
+ def create_standard_ticket_command(operation: str) -> Callable[..., str]:
520
525
  """Create a standard ticket operation command."""
521
526
 
522
527
  def command_template(
523
- ticket_id: Optional[str] = None,
524
- title: Optional[str] = None,
525
- description: Optional[str] = None,
526
- priority: Optional[Priority] = None,
527
- state: Optional[TicketState] = None,
528
- assignee: Optional[str] = None,
529
- tags: Optional[list[str]] = None,
530
- adapter: Optional[str] = None,
531
- ):
528
+ ticket_id: str | None = None,
529
+ title: str | None = None,
530
+ description: str | None = None,
531
+ priority: Priority | None = None,
532
+ state: TicketState | None = None,
533
+ assignee: str | None = None,
534
+ tags: list[str] | None = None,
535
+ adapter: str | None = None,
536
+ ) -> str:
532
537
  """Template for ticket commands."""
533
538
  # Build ticket data
534
539
  ticket_data = {}
@@ -567,11 +572,11 @@ class TicketCommands:
567
572
  @async_command
568
573
  @handle_adapter_errors
569
574
  async def list_tickets(
570
- adapter_instance,
571
- state: Optional[TicketState] = None,
572
- priority: Optional[Priority] = None,
575
+ adapter_instance: Any,
576
+ state: TicketState | None = None,
577
+ priority: Priority | None = None,
573
578
  limit: int = 10,
574
- ):
579
+ ) -> None:
575
580
  """List tickets with filters."""
576
581
  filters = {}
577
582
  if state:
@@ -586,13 +591,13 @@ class TicketCommands:
586
591
  @async_command
587
592
  @handle_adapter_errors
588
593
  async def show_ticket(
589
- adapter_instance, ticket_id: str, show_comments: bool = False
590
- ):
594
+ adapter_instance: Any, ticket_id: str, show_comments: bool = False
595
+ ) -> None:
591
596
  """Show ticket details."""
592
597
  ticket = await adapter_instance.read(ticket_id)
593
598
  if not ticket:
594
599
  console.print(f"[red]✗[/red] Ticket not found: {ticket_id}")
595
- raise typer.Exit(1)
600
+ raise typer.Exit(1) from None
596
601
 
597
602
  comments = None
598
603
  if show_comments:
@@ -603,11 +608,11 @@ class TicketCommands:
603
608
  @staticmethod
604
609
  def create_ticket(
605
610
  title: str,
606
- description: Optional[str] = None,
611
+ description: str | None = None,
607
612
  priority: Priority = Priority.MEDIUM,
608
- tags: Optional[list[str]] = None,
609
- assignee: Optional[str] = None,
610
- adapter: Optional[str] = None,
613
+ tags: list[str] | None = None,
614
+ assignee: str | None = None,
615
+ adapter: str | None = None,
611
616
  ) -> str:
612
617
  """Create a new ticket."""
613
618
  ticket_data = {
@@ -622,19 +627,19 @@ class TicketCommands:
622
627
 
623
628
  @staticmethod
624
629
  def update_ticket(
625
- ticket_id: str, updates: dict[str, Any], adapter: Optional[str] = None
630
+ ticket_id: str, updates: dict[str, Any], adapter: str | None = None
626
631
  ) -> str:
627
632
  """Update a ticket."""
628
633
  if not updates:
629
634
  console.print("[yellow]No updates specified[/yellow]")
630
- raise typer.Exit(1)
635
+ raise typer.Exit(1) from None
631
636
 
632
637
  updates["ticket_id"] = ticket_id
633
638
  return CommonPatterns.queue_operation(updates, "update", adapter)
634
639
 
635
640
  @staticmethod
636
641
  def transition_ticket(
637
- ticket_id: str, state: TicketState, adapter: Optional[str] = None
642
+ ticket_id: str, state: TicketState, adapter: str | None = None
638
643
  ) -> str:
639
644
  """Transition ticket state."""
640
645
  ticket_data = {
@@ -1,16 +1,29 @@
1
1
  """Core models and abstractions for MCP Ticketer."""
2
2
 
3
3
  from .adapter import BaseAdapter
4
- from .models import 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 Attachment, Comment, Epic, Priority, Task, TicketState, TicketType
5
12
  from .registry import AdapterRegistry
6
13
 
7
14
  __all__ = [
8
15
  "Epic",
9
16
  "Task",
10
17
  "Comment",
18
+ "Attachment",
11
19
  "TicketState",
12
20
  "Priority",
13
21
  "TicketType",
14
22
  "BaseAdapter",
15
23
  "AdapterRegistry",
24
+ "TicketInstructionsManager",
25
+ "InstructionsError",
26
+ "InstructionsNotFoundError",
27
+ "InstructionsValidationError",
28
+ "get_instructions",
16
29
  ]