mcp-ticketer 0.12.0__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
mcp_ticketer/__init__.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"""MCP Ticketer - Universal ticket management interface."""
|
|
2
2
|
|
|
3
3
|
from .__version__ import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
__author__,
|
|
5
|
+
__author_email__,
|
|
6
|
+
__copyright__,
|
|
7
|
+
__description__,
|
|
8
|
+
__license__,
|
|
9
|
+
__title__,
|
|
10
|
+
__version__,
|
|
11
|
+
__version_info__,
|
|
12
|
+
get_user_agent,
|
|
13
|
+
get_version,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
mcp_ticketer/__version__.py
CHANGED
|
@@ -23,14 +23,14 @@ logger = logging.getLogger(__name__)
|
|
|
23
23
|
|
|
24
24
|
# Import ai-trackdown-pytools when available
|
|
25
25
|
try:
|
|
26
|
-
from ai_trackdown_pytools import AITrackdown
|
|
27
|
-
from ai_trackdown_pytools import Ticket as AITicket
|
|
26
|
+
from ai_trackdown_pytools import AITrackdown # type: ignore[attr-defined]
|
|
27
|
+
from ai_trackdown_pytools import Ticket as AITicket # type: ignore[attr-defined]
|
|
28
28
|
|
|
29
29
|
HAS_AITRACKDOWN = True
|
|
30
30
|
except ImportError:
|
|
31
31
|
HAS_AITRACKDOWN = False
|
|
32
|
-
AITrackdown = None
|
|
33
|
-
AITicket = None
|
|
32
|
+
AITrackdown = None # type: ignore[assignment]
|
|
33
|
+
AITicket = None # type: ignore[assignment]
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class AITrackdownAdapter(BaseAdapter[Task]):
|
|
@@ -40,6 +40,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
40
40
|
"""Initialize AI-Trackdown adapter.
|
|
41
41
|
|
|
42
42
|
Args:
|
|
43
|
+
----
|
|
43
44
|
config: Configuration with 'base_path' for tickets directory
|
|
44
45
|
|
|
45
46
|
"""
|
|
@@ -64,6 +65,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
64
65
|
AITrackdown is file-based and doesn't require credentials.
|
|
65
66
|
|
|
66
67
|
Returns:
|
|
68
|
+
-------
|
|
67
69
|
(is_valid, error_message) - Always returns (True, "") for AITrackdown
|
|
68
70
|
|
|
69
71
|
"""
|
|
@@ -128,6 +130,8 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
128
130
|
parent_issue=ai_ticket.get("parent_issue"),
|
|
129
131
|
parent_epic=ai_ticket.get("parent_epic"),
|
|
130
132
|
assignee=ai_ticket.get("assignee"),
|
|
133
|
+
estimated_hours=ai_ticket.get("estimated_hours"),
|
|
134
|
+
actual_hours=ai_ticket.get("actual_hours"),
|
|
131
135
|
created_at=(
|
|
132
136
|
datetime.fromisoformat(ai_ticket["created_at"])
|
|
133
137
|
if "created_at" in ai_ticket
|
|
@@ -180,13 +184,15 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
180
184
|
"""Convert universal Task to AI-Trackdown ticket."""
|
|
181
185
|
# Handle enum values that may be stored as strings due to use_enum_values=True
|
|
182
186
|
# Note: task.state is always a string due to ConfigDict(use_enum_values=True)
|
|
183
|
-
state_value
|
|
187
|
+
state_value: str
|
|
184
188
|
if isinstance(task.state, TicketState):
|
|
185
189
|
state_value = self._get_state_mapping()[task.state]
|
|
186
190
|
elif isinstance(task.state, str):
|
|
187
191
|
# Already a string - keep as-is (don't convert to kebab-case)
|
|
188
192
|
# The state is already in snake_case format from the enum value
|
|
189
193
|
state_value = task.state
|
|
194
|
+
else:
|
|
195
|
+
state_value = str(task.state)
|
|
190
196
|
|
|
191
197
|
return {
|
|
192
198
|
"id": task.id,
|
|
@@ -208,13 +214,15 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
208
214
|
"""Convert universal Epic to AI-Trackdown ticket."""
|
|
209
215
|
# Handle enum values that may be stored as strings due to use_enum_values=True
|
|
210
216
|
# Note: epic.state is always a string due to ConfigDict(use_enum_values=True)
|
|
211
|
-
state_value
|
|
217
|
+
state_value: str
|
|
212
218
|
if isinstance(epic.state, TicketState):
|
|
213
219
|
state_value = self._get_state_mapping()[epic.state]
|
|
214
220
|
elif isinstance(epic.state, str):
|
|
215
221
|
# Already a string - keep as-is (don't convert to kebab-case)
|
|
216
222
|
# The state is already in snake_case format from the enum value
|
|
217
223
|
state_value = epic.state
|
|
224
|
+
else:
|
|
225
|
+
state_value = str(epic.state)
|
|
218
226
|
|
|
219
227
|
return {
|
|
220
228
|
"id": epic.id,
|
|
@@ -286,11 +294,13 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
286
294
|
"""Create a new epic.
|
|
287
295
|
|
|
288
296
|
Args:
|
|
297
|
+
----
|
|
289
298
|
title: Epic title
|
|
290
299
|
description: Epic description
|
|
291
300
|
**kwargs: Additional epic properties
|
|
292
301
|
|
|
293
302
|
Returns:
|
|
303
|
+
-------
|
|
294
304
|
Created Epic instance
|
|
295
305
|
|
|
296
306
|
"""
|
|
@@ -307,12 +317,14 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
307
317
|
"""Create a new issue.
|
|
308
318
|
|
|
309
319
|
Args:
|
|
320
|
+
----
|
|
310
321
|
title: Issue title
|
|
311
322
|
parent_epic: Parent epic ID
|
|
312
323
|
description: Issue description
|
|
313
324
|
**kwargs: Additional issue properties
|
|
314
325
|
|
|
315
326
|
Returns:
|
|
327
|
+
-------
|
|
316
328
|
Created Task instance (representing an issue)
|
|
317
329
|
|
|
318
330
|
"""
|
|
@@ -327,12 +339,14 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
327
339
|
"""Create a new task under an issue.
|
|
328
340
|
|
|
329
341
|
Args:
|
|
342
|
+
----
|
|
330
343
|
title: Task title
|
|
331
344
|
parent_id: Parent issue ID
|
|
332
345
|
description: Task description
|
|
333
346
|
**kwargs: Additional task properties
|
|
334
347
|
|
|
335
348
|
Returns:
|
|
349
|
+
-------
|
|
336
350
|
Created Task instance
|
|
337
351
|
|
|
338
352
|
"""
|
|
@@ -362,13 +376,16 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
362
376
|
"""Update a task or epic.
|
|
363
377
|
|
|
364
378
|
Args:
|
|
379
|
+
----
|
|
365
380
|
ticket_id: ID of ticket to update
|
|
366
381
|
updates: Dictionary of updates or Task object with new values
|
|
367
382
|
|
|
368
383
|
Returns:
|
|
384
|
+
-------
|
|
369
385
|
Updated Task or Epic, or None if ticket not found
|
|
370
386
|
|
|
371
387
|
Raises:
|
|
388
|
+
------
|
|
372
389
|
AttributeError: If update fails due to invalid fields
|
|
373
390
|
|
|
374
391
|
"""
|
|
@@ -555,9 +572,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
555
572
|
"""Get epic by ID.
|
|
556
573
|
|
|
557
574
|
Args:
|
|
575
|
+
----
|
|
558
576
|
epic_id: Epic ID to retrieve
|
|
559
577
|
|
|
560
578
|
Returns:
|
|
579
|
+
-------
|
|
561
580
|
Epic if found, None otherwise
|
|
562
581
|
|
|
563
582
|
"""
|
|
@@ -580,10 +599,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
580
599
|
"""List all epics.
|
|
581
600
|
|
|
582
601
|
Args:
|
|
602
|
+
----
|
|
583
603
|
limit: Maximum number of epics to return
|
|
584
604
|
offset: Number of epics to skip
|
|
585
605
|
|
|
586
606
|
Returns:
|
|
607
|
+
-------
|
|
587
608
|
List of epics
|
|
588
609
|
|
|
589
610
|
"""
|
|
@@ -598,9 +619,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
598
619
|
"""List all issues belonging to an epic.
|
|
599
620
|
|
|
600
621
|
Args:
|
|
622
|
+
----
|
|
601
623
|
epic_id: Epic ID to get issues for
|
|
602
624
|
|
|
603
625
|
Returns:
|
|
626
|
+
-------
|
|
604
627
|
List of issues (tasks with parent_epic set)
|
|
605
628
|
|
|
606
629
|
"""
|
|
@@ -615,9 +638,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
615
638
|
"""List all tasks belonging to an issue.
|
|
616
639
|
|
|
617
640
|
Args:
|
|
641
|
+
----
|
|
618
642
|
issue_id: Issue ID (parent task) to get child tasks for
|
|
619
643
|
|
|
620
644
|
Returns:
|
|
645
|
+
-------
|
|
621
646
|
List of tasks
|
|
622
647
|
|
|
623
648
|
"""
|
|
@@ -633,9 +658,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
633
658
|
"""Sanitize filename to prevent security issues.
|
|
634
659
|
|
|
635
660
|
Args:
|
|
661
|
+
----
|
|
636
662
|
filename: Original filename
|
|
637
663
|
|
|
638
664
|
Returns:
|
|
665
|
+
-------
|
|
639
666
|
Sanitized filename safe for filesystem
|
|
640
667
|
|
|
641
668
|
"""
|
|
@@ -655,9 +682,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
655
682
|
"""Guess MIME type from file extension.
|
|
656
683
|
|
|
657
684
|
Args:
|
|
685
|
+
----
|
|
658
686
|
file_path: Path to file
|
|
659
687
|
|
|
660
688
|
Returns:
|
|
689
|
+
-------
|
|
661
690
|
MIME type string
|
|
662
691
|
|
|
663
692
|
"""
|
|
@@ -670,9 +699,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
670
699
|
"""Calculate SHA256 checksum of file.
|
|
671
700
|
|
|
672
701
|
Args:
|
|
702
|
+
----
|
|
673
703
|
file_path: Path to file
|
|
674
704
|
|
|
675
705
|
Returns:
|
|
706
|
+
-------
|
|
676
707
|
Hexadecimal checksum string
|
|
677
708
|
|
|
678
709
|
"""
|
|
@@ -695,14 +726,17 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
695
726
|
"""Attach a file to a ticket (local filesystem storage).
|
|
696
727
|
|
|
697
728
|
Args:
|
|
729
|
+
----
|
|
698
730
|
ticket_id: Ticket identifier
|
|
699
731
|
file_path: Local file path to attach
|
|
700
732
|
description: Optional attachment description
|
|
701
733
|
|
|
702
734
|
Returns:
|
|
735
|
+
-------
|
|
703
736
|
Attachment metadata
|
|
704
737
|
|
|
705
738
|
Raises:
|
|
739
|
+
------
|
|
706
740
|
ValueError: If ticket doesn't exist
|
|
707
741
|
FileNotFoundError: If file doesn't exist
|
|
708
742
|
|
|
@@ -767,9 +801,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
767
801
|
"""Get all attachments for a ticket with path traversal protection.
|
|
768
802
|
|
|
769
803
|
Args:
|
|
804
|
+
----
|
|
770
805
|
ticket_id: Ticket identifier
|
|
771
806
|
|
|
772
807
|
Returns:
|
|
808
|
+
-------
|
|
773
809
|
List of attachments (empty if none)
|
|
774
810
|
|
|
775
811
|
"""
|
|
@@ -820,10 +856,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
820
856
|
"""Delete an attachment and its metadata with path traversal protection.
|
|
821
857
|
|
|
822
858
|
Args:
|
|
859
|
+
----
|
|
823
860
|
ticket_id: Ticket identifier
|
|
824
861
|
attachment_id: Attachment identifier
|
|
825
862
|
|
|
826
863
|
Returns:
|
|
864
|
+
-------
|
|
827
865
|
True if deleted, False if not found
|
|
828
866
|
|
|
829
867
|
"""
|
|
@@ -861,6 +899,347 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
861
899
|
|
|
862
900
|
return deleted
|
|
863
901
|
|
|
902
|
+
async def update_epic(self, epic_id: str, updates: dict[str, Any]) -> Epic | None:
|
|
903
|
+
"""Update an epic (project) in AITrackdown.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
----
|
|
907
|
+
epic_id: Epic identifier (filename without .json)
|
|
908
|
+
updates: Dictionary of fields to update. Supported fields:
|
|
909
|
+
- title: Epic title
|
|
910
|
+
- description: Epic description
|
|
911
|
+
- state: TicketState value
|
|
912
|
+
- priority: Priority value
|
|
913
|
+
- tags: List of tags
|
|
914
|
+
- target_date: Target completion date
|
|
915
|
+
- metadata: User metadata dictionary
|
|
916
|
+
|
|
917
|
+
Returns:
|
|
918
|
+
-------
|
|
919
|
+
Updated Epic object or None if epic not found
|
|
920
|
+
|
|
921
|
+
Raises:
|
|
922
|
+
------
|
|
923
|
+
ValueError: If epic_id is invalid or epic not found
|
|
924
|
+
|
|
925
|
+
Note:
|
|
926
|
+
----
|
|
927
|
+
AITrackdown stores epics as JSON files in {storage_path}/tickets/
|
|
928
|
+
Updates are applied as partial updates (only specified fields changed)
|
|
929
|
+
|
|
930
|
+
"""
|
|
931
|
+
# Validate epic_id
|
|
932
|
+
if not epic_id:
|
|
933
|
+
raise ValueError("epic_id is required")
|
|
934
|
+
|
|
935
|
+
# Read existing epic
|
|
936
|
+
existing = await self.read(epic_id)
|
|
937
|
+
if not existing:
|
|
938
|
+
logger.warning("Epic %s not found for update", epic_id)
|
|
939
|
+
return None
|
|
940
|
+
|
|
941
|
+
# Ensure it's an epic, not a task
|
|
942
|
+
if not isinstance(existing, Epic):
|
|
943
|
+
logger.warning("Ticket %s is not an epic", epic_id)
|
|
944
|
+
return None
|
|
945
|
+
|
|
946
|
+
# Apply updates to the existing epic
|
|
947
|
+
for key, value in updates.items():
|
|
948
|
+
if hasattr(existing, key) and value is not None:
|
|
949
|
+
setattr(existing, key, value)
|
|
950
|
+
|
|
951
|
+
# Update timestamp
|
|
952
|
+
existing.updated_at = datetime.now()
|
|
953
|
+
|
|
954
|
+
# Write back to file
|
|
955
|
+
ai_ticket = self._epic_to_ai_ticket(existing)
|
|
956
|
+
self._write_ticket_file(epic_id, ai_ticket)
|
|
957
|
+
|
|
958
|
+
logger.info("Updated epic %s with fields: %s", epic_id, list(updates.keys()))
|
|
959
|
+
return existing
|
|
960
|
+
|
|
961
|
+
async def list_labels(self, limit: int = 100) -> builtins.list[dict[str, Any]]:
|
|
962
|
+
"""List all tags (labels) used across tickets.
|
|
963
|
+
|
|
964
|
+
Args:
|
|
965
|
+
----
|
|
966
|
+
limit: Maximum number of labels to return (default: 100)
|
|
967
|
+
|
|
968
|
+
Returns:
|
|
969
|
+
-------
|
|
970
|
+
List of label dictionaries sorted by usage count (descending).
|
|
971
|
+
Each dictionary contains:
|
|
972
|
+
- id: Tag name (same as name in AITrackdown)
|
|
973
|
+
- name: Tag name
|
|
974
|
+
- count: Number of tickets using this tag
|
|
975
|
+
|
|
976
|
+
Note:
|
|
977
|
+
----
|
|
978
|
+
AITrackdown uses 'tags' terminology. This method scans
|
|
979
|
+
all task and epic files to extract unique tags.
|
|
980
|
+
|
|
981
|
+
"""
|
|
982
|
+
# Initialize tag counter
|
|
983
|
+
tag_counts: dict[str, int] = {}
|
|
984
|
+
|
|
985
|
+
# Scan all ticket JSON files
|
|
986
|
+
if self.tickets_dir.exists():
|
|
987
|
+
for ticket_file in self.tickets_dir.glob("*.json"):
|
|
988
|
+
try:
|
|
989
|
+
with open(ticket_file) as f:
|
|
990
|
+
ticket_data = json.load(f)
|
|
991
|
+
tags = ticket_data.get("tags", [])
|
|
992
|
+
for tag in tags:
|
|
993
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
994
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
995
|
+
logger.warning("Failed to read ticket file %s: %s", ticket_file, e)
|
|
996
|
+
continue
|
|
997
|
+
|
|
998
|
+
# Sort by usage count (descending)
|
|
999
|
+
sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
|
1000
|
+
|
|
1001
|
+
# Return top N tags with standardized format
|
|
1002
|
+
return [
|
|
1003
|
+
{"id": tag, "name": tag, "count": count}
|
|
1004
|
+
for tag, count in sorted_tags[:limit]
|
|
1005
|
+
]
|
|
1006
|
+
|
|
1007
|
+
async def create_issue_label(
|
|
1008
|
+
self, name: str, color: str | None = None
|
|
1009
|
+
) -> dict[str, Any]:
|
|
1010
|
+
"""Create/register a label (tag) in AITrackdown.
|
|
1011
|
+
|
|
1012
|
+
Args:
|
|
1013
|
+
----
|
|
1014
|
+
name: Label name (alphanumeric, hyphens, underscores allowed)
|
|
1015
|
+
color: Optional color (not used in file-based storage)
|
|
1016
|
+
|
|
1017
|
+
Returns:
|
|
1018
|
+
-------
|
|
1019
|
+
Label dictionary with:
|
|
1020
|
+
- id: Label name
|
|
1021
|
+
- name: Label name
|
|
1022
|
+
- color: Color value (if provided)
|
|
1023
|
+
- created: True (always, as tags are created on use)
|
|
1024
|
+
|
|
1025
|
+
Raises:
|
|
1026
|
+
------
|
|
1027
|
+
ValueError: If label name is invalid
|
|
1028
|
+
|
|
1029
|
+
Note:
|
|
1030
|
+
----
|
|
1031
|
+
AITrackdown creates tags implicitly when used on tickets.
|
|
1032
|
+
This method validates the tag name and returns success.
|
|
1033
|
+
Tags are stored as arrays in ticket JSON files.
|
|
1034
|
+
|
|
1035
|
+
"""
|
|
1036
|
+
# Validate tag name
|
|
1037
|
+
if not name:
|
|
1038
|
+
raise ValueError("Label name cannot be empty")
|
|
1039
|
+
|
|
1040
|
+
# Check for valid characters (alphanumeric, hyphens, underscores, spaces)
|
|
1041
|
+
import re
|
|
1042
|
+
|
|
1043
|
+
if not re.match(r"^[a-zA-Z0-9_\- ]+$", name):
|
|
1044
|
+
raise ValueError(
|
|
1045
|
+
"Label name must contain only alphanumeric characters, hyphens, underscores, or spaces"
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
# Return success response
|
|
1049
|
+
logger.info("Label '%s' registered (created implicitly on use)", name)
|
|
1050
|
+
return {
|
|
1051
|
+
"id": name,
|
|
1052
|
+
"name": name,
|
|
1053
|
+
"color": color,
|
|
1054
|
+
"created": True,
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
async def list_project_labels(
|
|
1058
|
+
self, epic_id: str, limit: int = 100
|
|
1059
|
+
) -> builtins.list[dict[str, Any]]:
|
|
1060
|
+
"""List labels (tags) used in a specific epic and its tasks.
|
|
1061
|
+
|
|
1062
|
+
Args:
|
|
1063
|
+
----
|
|
1064
|
+
epic_id: Epic identifier
|
|
1065
|
+
limit: Maximum number of labels to return (default: 100)
|
|
1066
|
+
|
|
1067
|
+
Returns:
|
|
1068
|
+
-------
|
|
1069
|
+
List of label dictionaries used in the epic, sorted by usage count.
|
|
1070
|
+
Each dictionary contains:
|
|
1071
|
+
- id: Tag name
|
|
1072
|
+
- name: Tag name
|
|
1073
|
+
- count: Number of tickets using this tag within the epic
|
|
1074
|
+
|
|
1075
|
+
Raises:
|
|
1076
|
+
------
|
|
1077
|
+
ValueError: If epic not found
|
|
1078
|
+
|
|
1079
|
+
Note:
|
|
1080
|
+
----
|
|
1081
|
+
Scans the epic and all tasks with parent_epic == epic_id.
|
|
1082
|
+
|
|
1083
|
+
"""
|
|
1084
|
+
# Validate epic exists
|
|
1085
|
+
epic = await self.get_epic(epic_id)
|
|
1086
|
+
if not epic:
|
|
1087
|
+
raise ValueError(f"Epic {epic_id} not found")
|
|
1088
|
+
|
|
1089
|
+
# Initialize tag counter
|
|
1090
|
+
tag_counts: dict[str, int] = {}
|
|
1091
|
+
|
|
1092
|
+
# Add tags from the epic itself
|
|
1093
|
+
if epic.tags:
|
|
1094
|
+
for tag in epic.tags:
|
|
1095
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
1096
|
+
|
|
1097
|
+
# Find all tasks with parent_epic == epic_id
|
|
1098
|
+
all_tasks = await self.list_issues_by_epic(epic_id)
|
|
1099
|
+
for task in all_tasks:
|
|
1100
|
+
if task.tags:
|
|
1101
|
+
for tag in task.tags:
|
|
1102
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
1103
|
+
|
|
1104
|
+
# Sort by usage count (descending)
|
|
1105
|
+
sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
|
1106
|
+
|
|
1107
|
+
# Return top N tags
|
|
1108
|
+
return [
|
|
1109
|
+
{"id": tag, "name": tag, "count": count}
|
|
1110
|
+
for tag, count in sorted_tags[:limit]
|
|
1111
|
+
]
|
|
1112
|
+
|
|
1113
|
+
async def list_cycles(self, limit: int = 50) -> builtins.list[dict[str, Any]]:
|
|
1114
|
+
"""List cycles (sprints) - Not supported in file-based AITrackdown.
|
|
1115
|
+
|
|
1116
|
+
Args:
|
|
1117
|
+
----
|
|
1118
|
+
limit: Maximum number of cycles to return (unused)
|
|
1119
|
+
|
|
1120
|
+
Returns:
|
|
1121
|
+
-------
|
|
1122
|
+
Empty list (cycles not supported)
|
|
1123
|
+
|
|
1124
|
+
Note:
|
|
1125
|
+
----
|
|
1126
|
+
AITrackdown is a simple file-based system without
|
|
1127
|
+
cycle/sprint management. Returns empty list.
|
|
1128
|
+
|
|
1129
|
+
"""
|
|
1130
|
+
logger.info("list_cycles called but cycles not supported in AITrackdown")
|
|
1131
|
+
return []
|
|
1132
|
+
|
|
1133
|
+
async def get_issue_status(self, ticket_id: str) -> dict[str, Any] | None:
|
|
1134
|
+
"""Get status details for a ticket.
|
|
1135
|
+
|
|
1136
|
+
Args:
|
|
1137
|
+
----
|
|
1138
|
+
ticket_id: Ticket identifier
|
|
1139
|
+
|
|
1140
|
+
Returns:
|
|
1141
|
+
-------
|
|
1142
|
+
Status dictionary with:
|
|
1143
|
+
- id: Ticket ID
|
|
1144
|
+
- state: Current state
|
|
1145
|
+
- priority: Current priority
|
|
1146
|
+
- updated_at: Last update timestamp
|
|
1147
|
+
- created_at: Creation timestamp
|
|
1148
|
+
- title: Ticket title
|
|
1149
|
+
- assignee: Assignee (if Task, None for Epic)
|
|
1150
|
+
Returns None if ticket not found
|
|
1151
|
+
|
|
1152
|
+
Raises:
|
|
1153
|
+
------
|
|
1154
|
+
ValueError: If ticket_id is invalid
|
|
1155
|
+
|
|
1156
|
+
"""
|
|
1157
|
+
if not ticket_id:
|
|
1158
|
+
raise ValueError("ticket_id is required")
|
|
1159
|
+
|
|
1160
|
+
# Read ticket
|
|
1161
|
+
ticket = await self.read(ticket_id)
|
|
1162
|
+
if not ticket:
|
|
1163
|
+
logger.warning("Ticket %s not found", ticket_id)
|
|
1164
|
+
return None
|
|
1165
|
+
|
|
1166
|
+
# Return comprehensive status object
|
|
1167
|
+
status = {
|
|
1168
|
+
"id": ticket.id,
|
|
1169
|
+
"state": ticket.state,
|
|
1170
|
+
"priority": ticket.priority,
|
|
1171
|
+
"updated_at": ticket.updated_at.isoformat() if ticket.updated_at else None,
|
|
1172
|
+
"created_at": ticket.created_at.isoformat() if ticket.created_at else None,
|
|
1173
|
+
"title": ticket.title,
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
# Add assignee only if ticket is a Task (Epic doesn't have assignee)
|
|
1177
|
+
if hasattr(ticket, "assignee"):
|
|
1178
|
+
status["assignee"] = ticket.assignee
|
|
1179
|
+
|
|
1180
|
+
return status
|
|
1181
|
+
|
|
1182
|
+
async def list_issue_statuses(self) -> builtins.list[dict[str, Any]]:
|
|
1183
|
+
"""List available ticket statuses.
|
|
1184
|
+
|
|
1185
|
+
Returns:
|
|
1186
|
+
-------
|
|
1187
|
+
List of status dictionaries with:
|
|
1188
|
+
- id: State identifier
|
|
1189
|
+
- name: Human-readable state name
|
|
1190
|
+
- description: State description
|
|
1191
|
+
|
|
1192
|
+
Note:
|
|
1193
|
+
----
|
|
1194
|
+
AITrackdown uses standard TicketState enum values:
|
|
1195
|
+
open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
1196
|
+
|
|
1197
|
+
"""
|
|
1198
|
+
# Return hardcoded list of TicketState values
|
|
1199
|
+
statuses = [
|
|
1200
|
+
{
|
|
1201
|
+
"id": "open",
|
|
1202
|
+
"name": "Open",
|
|
1203
|
+
"description": "Ticket is created and ready to be worked on",
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
"id": "in_progress",
|
|
1207
|
+
"name": "In Progress",
|
|
1208
|
+
"description": "Ticket is actively being worked on",
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
"id": "ready",
|
|
1212
|
+
"name": "Ready",
|
|
1213
|
+
"description": "Ticket is ready for review or testing",
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
"id": "tested",
|
|
1217
|
+
"name": "Tested",
|
|
1218
|
+
"description": "Ticket has been tested and verified",
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
"id": "done",
|
|
1222
|
+
"name": "Done",
|
|
1223
|
+
"description": "Ticket work is completed",
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
"id": "closed",
|
|
1227
|
+
"name": "Closed",
|
|
1228
|
+
"description": "Ticket is closed and archived",
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
"id": "waiting",
|
|
1232
|
+
"name": "Waiting",
|
|
1233
|
+
"description": "Ticket is waiting for external dependency",
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
"id": "blocked",
|
|
1237
|
+
"name": "Blocked",
|
|
1238
|
+
"description": "Ticket is blocked by an issue or dependency",
|
|
1239
|
+
},
|
|
1240
|
+
]
|
|
1241
|
+
return statuses
|
|
1242
|
+
|
|
864
1243
|
|
|
865
1244
|
# Register the adapter
|
|
866
1245
|
AdapterRegistry.register("aitrackdown", AITrackdownAdapter)
|