mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) 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 +796 -46
  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 +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,16 @@
1
1
  """Base adapter abstract class for ticket systems."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import builtins
4
6
  from abc import ABC, abstractmethod
5
- from typing import Any, Generic, Optional, TypeVar
7
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar
6
8
 
7
9
  from .models import Comment, Epic, SearchQuery, Task, TicketState, TicketType
10
+ from .state_matcher import get_state_matcher
11
+
12
+ if TYPE_CHECKING:
13
+ from .models import Attachment
8
14
 
9
15
  # Generic type for tickets
10
16
  T = TypeVar("T", Epic, Task)
@@ -17,17 +23,52 @@ class BaseAdapter(ABC, Generic[T]):
17
23
  """Initialize adapter with configuration.
18
24
 
19
25
  Args:
26
+ ----
20
27
  config: Adapter-specific configuration dictionary
21
28
 
22
29
  """
23
30
  self.config = config
24
31
  self._state_mapping = self._get_state_mapping()
25
32
 
33
+ @property
34
+ def adapter_type(self) -> str:
35
+ """Return lowercase adapter type identifier.
36
+
37
+ This identifier is used in MCP responses to show which adapter
38
+ handled the operation (e.g., "linear", "github", "jira", "asana").
39
+
40
+ Returns:
41
+ -------
42
+ Lowercase adapter type (e.g., "linear", "github")
43
+
44
+ """
45
+ # Extract adapter type from class name
46
+ # LinearAdapter -> linear, GitHubAdapter -> github
47
+ class_name = self.__class__.__name__
48
+ if class_name.endswith("Adapter"):
49
+ adapter_name = class_name[: -len("Adapter")]
50
+ else:
51
+ adapter_name = class_name
52
+
53
+ return adapter_name.lower()
54
+
55
+ @property
56
+ def adapter_display_name(self) -> str:
57
+ """Return human-readable adapter name.
58
+
59
+ Returns:
60
+ -------
61
+ Title-cased adapter name (e.g., "Linear", "Github", "Jira")
62
+
63
+ """
64
+ return self.adapter_type.title()
65
+
26
66
  @abstractmethod
27
67
  def _get_state_mapping(self) -> dict[TicketState, str]:
28
68
  """Get mapping from universal states to system-specific states.
29
69
 
30
70
  Returns:
71
+ -------
31
72
  Dictionary mapping TicketState to system-specific state strings
32
73
 
33
74
  """
@@ -38,6 +79,7 @@ class BaseAdapter(ABC, Generic[T]):
38
79
  """Validate that required credentials are present.
39
80
 
40
81
  Returns:
82
+ -------
41
83
  (is_valid, error_message) - Tuple of validation result and error message
42
84
 
43
85
  """
@@ -48,36 +90,42 @@ class BaseAdapter(ABC, Generic[T]):
48
90
  """Create a new ticket.
49
91
 
50
92
  Args:
93
+ ----
51
94
  ticket: Ticket to create (Epic or Task)
52
95
 
53
96
  Returns:
97
+ -------
54
98
  Created ticket with ID populated
55
99
 
56
100
  """
57
101
  pass
58
102
 
59
103
  @abstractmethod
60
- async def read(self, ticket_id: str) -> Optional[T]:
104
+ async def read(self, ticket_id: str) -> T | None:
61
105
  """Read a ticket by ID.
62
106
 
63
107
  Args:
108
+ ----
64
109
  ticket_id: Unique ticket identifier
65
110
 
66
111
  Returns:
112
+ -------
67
113
  Ticket if found, None otherwise
68
114
 
69
115
  """
70
116
  pass
71
117
 
72
118
  @abstractmethod
73
- async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[T]:
119
+ async def update(self, ticket_id: str, updates: dict[str, Any]) -> T | None:
74
120
  """Update a ticket.
75
121
 
76
122
  Args:
123
+ ----
77
124
  ticket_id: Ticket identifier
78
125
  updates: Fields to update
79
126
 
80
127
  Returns:
128
+ -------
81
129
  Updated ticket if successful, None otherwise
82
130
 
83
131
  """
@@ -88,9 +136,11 @@ class BaseAdapter(ABC, Generic[T]):
88
136
  """Delete a ticket.
89
137
 
90
138
  Args:
139
+ ----
91
140
  ticket_id: Ticket identifier
92
141
 
93
142
  Returns:
143
+ -------
94
144
  True if deleted, False otherwise
95
145
 
96
146
  """
@@ -98,16 +148,18 @@ class BaseAdapter(ABC, Generic[T]):
98
148
 
99
149
  @abstractmethod
100
150
  async def list(
101
- self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
151
+ self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
102
152
  ) -> list[T]:
103
153
  """List tickets with pagination and filters.
104
154
 
105
155
  Args:
156
+ ----
106
157
  limit: Maximum number of tickets
107
158
  offset: Skip this many tickets
108
159
  filters: Optional filter criteria
109
160
 
110
161
  Returns:
162
+ -------
111
163
  List of tickets matching criteria
112
164
 
113
165
  """
@@ -118,9 +170,11 @@ class BaseAdapter(ABC, Generic[T]):
118
170
  """Search tickets using advanced query.
119
171
 
120
172
  Args:
173
+ ----
121
174
  query: Search parameters
122
175
 
123
176
  Returns:
177
+ -------
124
178
  List of tickets matching search criteria
125
179
 
126
180
  """
@@ -129,14 +183,16 @@ class BaseAdapter(ABC, Generic[T]):
129
183
  @abstractmethod
130
184
  async def transition_state(
131
185
  self, ticket_id: str, target_state: TicketState
132
- ) -> Optional[T]:
186
+ ) -> T | None:
133
187
  """Transition ticket to a new state.
134
188
 
135
189
  Args:
190
+ ----
136
191
  ticket_id: Ticket identifier
137
192
  target_state: Target state
138
193
 
139
194
  Returns:
195
+ -------
140
196
  Updated ticket if transition successful, None otherwise
141
197
 
142
198
  """
@@ -147,9 +203,11 @@ class BaseAdapter(ABC, Generic[T]):
147
203
  """Add a comment to a ticket.
148
204
 
149
205
  Args:
206
+ ----
150
207
  comment: Comment to add
151
208
 
152
209
  Returns:
210
+ -------
153
211
  Created comment with ID populated
154
212
 
155
213
  """
@@ -162,11 +220,13 @@ class BaseAdapter(ABC, Generic[T]):
162
220
  """Get comments for a ticket.
163
221
 
164
222
  Args:
223
+ ----
165
224
  ticket_id: Ticket identifier
166
225
  limit: Maximum number of comments
167
226
  offset: Skip this many comments
168
227
 
169
228
  Returns:
229
+ -------
170
230
  List of comments for the ticket
171
231
 
172
232
  """
@@ -176,9 +236,11 @@ class BaseAdapter(ABC, Generic[T]):
176
236
  """Map universal state to system-specific state.
177
237
 
178
238
  Args:
239
+ ----
179
240
  state: Universal ticket state
180
241
 
181
242
  Returns:
243
+ -------
182
244
  System-specific state string
183
245
 
184
246
  """
@@ -188,31 +250,88 @@ class BaseAdapter(ABC, Generic[T]):
188
250
  """Map system-specific state to universal state.
189
251
 
190
252
  Args:
253
+ ----
191
254
  system_state: System-specific state string
192
255
 
193
256
  Returns:
257
+ -------
194
258
  Universal ticket state
195
259
 
196
260
  """
197
261
  reverse_mapping = {v: k for k, v in self._state_mapping.items()}
198
262
  return reverse_mapping.get(system_state, TicketState.OPEN)
199
263
 
264
+ def get_available_states(self) -> list[str]:
265
+ """Get list of adapter-specific available states.
266
+
267
+ Returns adapter-specific state names that can be used for
268
+ semantic state matching. Override in subclasses to provide
269
+ platform-specific state names.
270
+
271
+ Returns:
272
+ -------
273
+ List of adapter-specific state names
274
+
275
+ Example:
276
+ -------
277
+ >>> # Linear adapter override
278
+ >>> def get_available_states(self):
279
+ ... return ["Backlog", "Todo", "In Progress", "Done", "Canceled"]
280
+
281
+ """
282
+ # Default: return universal state values
283
+ return [state.value for state in TicketState]
284
+
285
+ def resolve_state(self, user_input: str) -> TicketState:
286
+ """Resolve user input to universal state using semantic matcher.
287
+
288
+ Uses the semantic state matcher to interpret natural language
289
+ inputs and resolve them to universal TicketState values.
290
+
291
+ Args:
292
+ ----
293
+ user_input: Natural language state input (e.g., "working on it")
294
+
295
+ Returns:
296
+ -------
297
+ Resolved universal TicketState
298
+
299
+ Example:
300
+ -------
301
+ >>> adapter = get_adapter()
302
+ >>> state = adapter.resolve_state("working on it")
303
+ >>> print(state)
304
+ TicketState.IN_PROGRESS
305
+
306
+ """
307
+ matcher = get_state_matcher()
308
+ adapter_states = self.get_available_states()
309
+ result = matcher.match_state(user_input, adapter_states)
310
+ return result.state
311
+
200
312
  async def validate_transition(
201
313
  self, ticket_id: str, target_state: TicketState
202
314
  ) -> bool:
203
315
  """Validate if state transition is allowed.
204
316
 
317
+ Validates both workflow rules and parent/child state constraints:
318
+ - Parent issues must remain at least as complete as their most complete child
319
+ - Standard workflow transitions must be valid
320
+
205
321
  Args:
322
+ ----
206
323
  ticket_id: Ticket identifier
207
324
  target_state: Target state
208
325
 
209
326
  Returns:
327
+ -------
210
328
  True if transition is valid
211
329
 
212
330
  """
213
331
  ticket = await self.read(ticket_id)
214
332
  if not ticket:
215
333
  return False
334
+
216
335
  # Handle case where state might be stored as string due to use_enum_values=True
217
336
  current_state = ticket.state
218
337
  if isinstance(current_state, str):
@@ -220,21 +339,51 @@ class BaseAdapter(ABC, Generic[T]):
220
339
  current_state = TicketState(current_state)
221
340
  except ValueError:
222
341
  return False
223
- return current_state.can_transition_to(target_state)
342
+
343
+ # Check workflow transition validity
344
+ if not current_state.can_transition_to(target_state):
345
+ return False
346
+
347
+ # Check parent/child state constraint
348
+ # If this ticket has children, ensure target state >= max child state
349
+ if isinstance(ticket, Task):
350
+ # Get all children
351
+ children = await self.list_tasks_by_issue(ticket_id)
352
+ if children:
353
+ # Find max child completion level
354
+ max_child_level = 0
355
+ for child in children:
356
+ child_state = child.state
357
+ if isinstance(child_state, str):
358
+ try:
359
+ child_state = TicketState(child_state)
360
+ except ValueError:
361
+ continue
362
+ max_child_level = max(
363
+ max_child_level, child_state.completion_level()
364
+ )
365
+
366
+ # Target state must be at least as complete as most complete child
367
+ if target_state.completion_level() < max_child_level:
368
+ return False
369
+
370
+ return True
224
371
 
225
372
  # Epic/Issue/Task Hierarchy Methods
226
373
 
227
374
  async def create_epic(
228
- self, title: str, description: Optional[str] = None, **kwargs
229
- ) -> Optional[Epic]:
375
+ self, title: str, description: str | None = None, **kwargs: Any
376
+ ) -> Epic | None:
230
377
  """Create epic (top-level grouping).
231
378
 
232
379
  Args:
380
+ ----
233
381
  title: Epic title
234
382
  description: Epic description
235
383
  **kwargs: Additional adapter-specific fields
236
384
 
237
385
  Returns:
386
+ -------
238
387
  Created epic or None if failed
239
388
 
240
389
  """
@@ -249,13 +398,15 @@ class BaseAdapter(ABC, Generic[T]):
249
398
  return result
250
399
  return None
251
400
 
252
- async def get_epic(self, epic_id: str) -> Optional[Epic]:
401
+ async def get_epic(self, epic_id: str) -> Epic | None:
253
402
  """Get epic by ID.
254
403
 
255
404
  Args:
405
+ ----
256
406
  epic_id: Epic identifier
257
407
 
258
408
  Returns:
409
+ -------
259
410
  Epic if found, None otherwise
260
411
 
261
412
  """
@@ -265,13 +416,15 @@ class BaseAdapter(ABC, Generic[T]):
265
416
  return result
266
417
  return None
267
418
 
268
- async def list_epics(self, **kwargs) -> builtins.list[Epic]:
419
+ async def list_epics(self, **kwargs: Any) -> builtins.list[Epic]:
269
420
  """List all epics.
270
421
 
271
422
  Args:
423
+ ----
272
424
  **kwargs: Adapter-specific filter parameters
273
425
 
274
426
  Returns:
427
+ -------
275
428
  List of epics
276
429
 
277
430
  """
@@ -284,19 +437,21 @@ class BaseAdapter(ABC, Generic[T]):
284
437
  async def create_issue(
285
438
  self,
286
439
  title: str,
287
- description: Optional[str] = None,
288
- epic_id: Optional[str] = None,
289
- **kwargs,
290
- ) -> Optional[Task]:
440
+ description: str | None = None,
441
+ epic_id: str | None = None,
442
+ **kwargs: Any,
443
+ ) -> Task | None:
291
444
  """Create issue, optionally linked to epic.
292
445
 
293
446
  Args:
447
+ ----
294
448
  title: Issue title
295
449
  description: Issue description
296
450
  epic_id: Optional parent epic ID
297
451
  **kwargs: Additional adapter-specific fields
298
452
 
299
453
  Returns:
454
+ -------
300
455
  Created issue or None if failed
301
456
 
302
457
  """
@@ -313,9 +468,11 @@ class BaseAdapter(ABC, Generic[T]):
313
468
  """List all issues in epic.
314
469
 
315
470
  Args:
471
+ ----
316
472
  epic_id: Epic identifier
317
473
 
318
474
  Returns:
475
+ -------
319
476
  List of issues belonging to epic
320
477
 
321
478
  """
@@ -325,20 +482,23 @@ class BaseAdapter(ABC, Generic[T]):
325
482
  return [r for r in results if isinstance(r, Task) and r.is_issue()]
326
483
 
327
484
  async def create_task(
328
- self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
329
- ) -> Optional[Task]:
485
+ self, title: str, parent_id: str, description: str | None = None, **kwargs: Any
486
+ ) -> Task | None:
330
487
  """Create task as sub-ticket of parent issue.
331
488
 
332
489
  Args:
490
+ ----
333
491
  title: Task title
334
492
  parent_id: Required parent issue ID
335
493
  description: Task description
336
494
  **kwargs: Additional adapter-specific fields
337
495
 
338
496
  Returns:
497
+ -------
339
498
  Created task or None if failed
340
499
 
341
500
  Raises:
501
+ ------
342
502
  ValueError: If parent_id is not provided
343
503
 
344
504
  """
@@ -364,9 +524,11 @@ class BaseAdapter(ABC, Generic[T]):
364
524
  """List all tasks under an issue.
365
525
 
366
526
  Args:
527
+ ----
367
528
  issue_id: Issue identifier
368
529
 
369
530
  Returns:
531
+ -------
370
532
  List of tasks belonging to issue
371
533
 
372
534
  """
@@ -375,6 +537,78 @@ class BaseAdapter(ABC, Generic[T]):
375
537
  results = await self.list(filters=filters)
376
538
  return [r for r in results if isinstance(r, Task) and r.is_task()]
377
539
 
540
+ # Attachment methods
541
+ async def add_attachment(
542
+ self,
543
+ ticket_id: str,
544
+ file_path: str,
545
+ description: str | None = None,
546
+ ) -> Attachment:
547
+ """Attach a file to a ticket.
548
+
549
+ Args:
550
+ ----
551
+ ticket_id: Ticket identifier
552
+ file_path: Local file path to upload
553
+ description: Optional attachment description
554
+
555
+ Returns:
556
+ -------
557
+ Created Attachment with metadata
558
+
559
+ Raises:
560
+ ------
561
+ NotImplementedError: If adapter doesn't support attachments
562
+ FileNotFoundError: If file doesn't exist
563
+ ValueError: If ticket doesn't exist or upload fails
564
+
565
+ """
566
+ raise NotImplementedError(
567
+ f"{self.__class__.__name__} does not support file attachments. "
568
+ "Use comments to reference external files instead."
569
+ )
570
+
571
+ async def get_attachments(self, ticket_id: str) -> list[Attachment]:
572
+ """Get all attachments for a ticket.
573
+
574
+ Args:
575
+ ----
576
+ ticket_id: Ticket identifier
577
+
578
+ Returns:
579
+ -------
580
+ List of attachments (empty if none or not supported)
581
+
582
+ """
583
+ raise NotImplementedError(
584
+ f"{self.__class__.__name__} does not support file attachments."
585
+ )
586
+
587
+ async def delete_attachment(
588
+ self,
589
+ ticket_id: str,
590
+ attachment_id: str,
591
+ ) -> bool:
592
+ """Delete an attachment (optional implementation).
593
+
594
+ Args:
595
+ ----
596
+ ticket_id: Ticket identifier
597
+ attachment_id: Attachment identifier
598
+
599
+ Returns:
600
+ -------
601
+ True if deleted, False otherwise
602
+
603
+ Raises:
604
+ ------
605
+ NotImplementedError: If adapter doesn't support deletion
606
+
607
+ """
608
+ raise NotImplementedError(
609
+ f"{self.__class__.__name__} does not support attachment deletion."
610
+ )
611
+
378
612
  async def close(self) -> None:
379
613
  """Close adapter and cleanup resources."""
380
614
  pass