mcp-ticketer 0.12.0__py3-none-any.whl → 2.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/aitrackdown.py +507 -6
- mcp_ticketer/adapters/asana/adapter.py +229 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github/__init__.py +26 -0
- mcp_ticketer/adapters/github/adapter.py +3229 -0
- mcp_ticketer/adapters/github/client.py +335 -0
- mcp_ticketer/adapters/github/mappers.py +797 -0
- mcp_ticketer/adapters/github/queries.py +692 -0
- mcp_ticketer/adapters/github/types.py +460 -0
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira/__init__.py +35 -0
- mcp_ticketer/adapters/jira/adapter.py +1351 -0
- mcp_ticketer/adapters/jira/client.py +271 -0
- mcp_ticketer/adapters/jira/mappers.py +246 -0
- mcp_ticketer/adapters/jira/queries.py +216 -0
- mcp_ticketer/adapters/jira/types.py +304 -0
- mcp_ticketer/adapters/linear/adapter.py +2730 -139
- mcp_ticketer/adapters/linear/client.py +175 -3
- mcp_ticketer/adapters/linear/mappers.py +203 -8
- mcp_ticketer/adapters/linear/queries.py +280 -3
- mcp_ticketer/adapters/linear/types.py +120 -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 +1288 -105
- 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/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +267 -3175
- mcp_ticketer/cli/mcp_configure.py +821 -119
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +795 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +705 -103
- mcp_ticketer/cli/utils.py +113 -0
- mcp_ticketer/core/__init__.py +56 -6
- mcp_ticketer/core/adapter.py +533 -2
- 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/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +480 -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/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/session_state.py +176 -0
- mcp_ticketer/core/state_matcher.py +625 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/__main__.py +2 -1
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +33 -11
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +5 -5
- 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 +1391 -145
- 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/milestone_tools.py +338 -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 +209 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1107 -124
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/queue.py +68 -0
- 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.2.13.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.13.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.13.dist-info/top_level.txt +2 -0
- py_mcp_installer/examples/phase3_demo.py +178 -0
- py_mcp_installer/scripts/manage_version.py +54 -0
- py_mcp_installer/setup.py +6 -0
- py_mcp_installer/src/py_mcp_installer/__init__.py +153 -0
- py_mcp_installer/src/py_mcp_installer/command_builder.py +445 -0
- py_mcp_installer/src/py_mcp_installer/config_manager.py +541 -0
- py_mcp_installer/src/py_mcp_installer/exceptions.py +243 -0
- py_mcp_installer/src/py_mcp_installer/installation_strategy.py +617 -0
- py_mcp_installer/src/py_mcp_installer/installer.py +656 -0
- py_mcp_installer/src/py_mcp_installer/mcp_inspector.py +750 -0
- py_mcp_installer/src/py_mcp_installer/platform_detector.py +451 -0
- py_mcp_installer/src/py_mcp_installer/platforms/__init__.py +26 -0
- py_mcp_installer/src/py_mcp_installer/platforms/claude_code.py +225 -0
- py_mcp_installer/src/py_mcp_installer/platforms/codex.py +181 -0
- py_mcp_installer/src/py_mcp_installer/platforms/cursor.py +191 -0
- py_mcp_installer/src/py_mcp_installer/types.py +222 -0
- py_mcp_installer/src/py_mcp_installer/utils.py +463 -0
- py_mcp_installer/tests/__init__.py +0 -0
- py_mcp_installer/tests/platforms/__init__.py +0 -0
- py_mcp_installer/tests/test_platform_detector.py +17 -0
- mcp_ticketer/adapters/github.py +0 -1574
- mcp_ticketer/adapters/jira.py +0 -1258
- 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/top_level.txt +0 -1
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.2.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""AI-Trackdown adapter implementation."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import builtins
|
|
4
6
|
import json
|
|
5
7
|
import logging
|
|
@@ -23,14 +25,14 @@ logger = logging.getLogger(__name__)
|
|
|
23
25
|
|
|
24
26
|
# Import ai-trackdown-pytools when available
|
|
25
27
|
try:
|
|
26
|
-
from ai_trackdown_pytools import AITrackdown
|
|
27
|
-
from ai_trackdown_pytools import Ticket as AITicket
|
|
28
|
+
from ai_trackdown_pytools import AITrackdown # type: ignore[attr-defined]
|
|
29
|
+
from ai_trackdown_pytools import Ticket as AITicket # type: ignore[attr-defined]
|
|
28
30
|
|
|
29
31
|
HAS_AITRACKDOWN = True
|
|
30
32
|
except ImportError:
|
|
31
33
|
HAS_AITRACKDOWN = False
|
|
32
|
-
AITrackdown = None
|
|
33
|
-
AITicket = None
|
|
34
|
+
AITrackdown = None # type: ignore[assignment]
|
|
35
|
+
AITicket = None # type: ignore[assignment]
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class AITrackdownAdapter(BaseAdapter[Task]):
|
|
@@ -40,6 +42,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
40
42
|
"""Initialize AI-Trackdown adapter.
|
|
41
43
|
|
|
42
44
|
Args:
|
|
45
|
+
----
|
|
43
46
|
config: Configuration with 'base_path' for tickets directory
|
|
44
47
|
|
|
45
48
|
"""
|
|
@@ -64,6 +67,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
64
67
|
AITrackdown is file-based and doesn't require credentials.
|
|
65
68
|
|
|
66
69
|
Returns:
|
|
70
|
+
-------
|
|
67
71
|
(is_valid, error_message) - Always returns (True, "") for AITrackdown
|
|
68
72
|
|
|
69
73
|
"""
|
|
@@ -128,6 +132,8 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
128
132
|
parent_issue=ai_ticket.get("parent_issue"),
|
|
129
133
|
parent_epic=ai_ticket.get("parent_epic"),
|
|
130
134
|
assignee=ai_ticket.get("assignee"),
|
|
135
|
+
estimated_hours=ai_ticket.get("estimated_hours"),
|
|
136
|
+
actual_hours=ai_ticket.get("actual_hours"),
|
|
131
137
|
created_at=(
|
|
132
138
|
datetime.fromisoformat(ai_ticket["created_at"])
|
|
133
139
|
if "created_at" in ai_ticket
|
|
@@ -180,13 +186,15 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
180
186
|
"""Convert universal Task to AI-Trackdown ticket."""
|
|
181
187
|
# Handle enum values that may be stored as strings due to use_enum_values=True
|
|
182
188
|
# Note: task.state is always a string due to ConfigDict(use_enum_values=True)
|
|
183
|
-
state_value
|
|
189
|
+
state_value: str
|
|
184
190
|
if isinstance(task.state, TicketState):
|
|
185
191
|
state_value = self._get_state_mapping()[task.state]
|
|
186
192
|
elif isinstance(task.state, str):
|
|
187
193
|
# Already a string - keep as-is (don't convert to kebab-case)
|
|
188
194
|
# The state is already in snake_case format from the enum value
|
|
189
195
|
state_value = task.state
|
|
196
|
+
else:
|
|
197
|
+
state_value = str(task.state)
|
|
190
198
|
|
|
191
199
|
return {
|
|
192
200
|
"id": task.id,
|
|
@@ -208,13 +216,15 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
208
216
|
"""Convert universal Epic to AI-Trackdown ticket."""
|
|
209
217
|
# Handle enum values that may be stored as strings due to use_enum_values=True
|
|
210
218
|
# Note: epic.state is always a string due to ConfigDict(use_enum_values=True)
|
|
211
|
-
state_value
|
|
219
|
+
state_value: str
|
|
212
220
|
if isinstance(epic.state, TicketState):
|
|
213
221
|
state_value = self._get_state_mapping()[epic.state]
|
|
214
222
|
elif isinstance(epic.state, str):
|
|
215
223
|
# Already a string - keep as-is (don't convert to kebab-case)
|
|
216
224
|
# The state is already in snake_case format from the enum value
|
|
217
225
|
state_value = epic.state
|
|
226
|
+
else:
|
|
227
|
+
state_value = str(epic.state)
|
|
218
228
|
|
|
219
229
|
return {
|
|
220
230
|
"id": epic.id,
|
|
@@ -286,11 +296,13 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
286
296
|
"""Create a new epic.
|
|
287
297
|
|
|
288
298
|
Args:
|
|
299
|
+
----
|
|
289
300
|
title: Epic title
|
|
290
301
|
description: Epic description
|
|
291
302
|
**kwargs: Additional epic properties
|
|
292
303
|
|
|
293
304
|
Returns:
|
|
305
|
+
-------
|
|
294
306
|
Created Epic instance
|
|
295
307
|
|
|
296
308
|
"""
|
|
@@ -307,12 +319,14 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
307
319
|
"""Create a new issue.
|
|
308
320
|
|
|
309
321
|
Args:
|
|
322
|
+
----
|
|
310
323
|
title: Issue title
|
|
311
324
|
parent_epic: Parent epic ID
|
|
312
325
|
description: Issue description
|
|
313
326
|
**kwargs: Additional issue properties
|
|
314
327
|
|
|
315
328
|
Returns:
|
|
329
|
+
-------
|
|
316
330
|
Created Task instance (representing an issue)
|
|
317
331
|
|
|
318
332
|
"""
|
|
@@ -327,12 +341,14 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
327
341
|
"""Create a new task under an issue.
|
|
328
342
|
|
|
329
343
|
Args:
|
|
344
|
+
----
|
|
330
345
|
title: Task title
|
|
331
346
|
parent_id: Parent issue ID
|
|
332
347
|
description: Task description
|
|
333
348
|
**kwargs: Additional task properties
|
|
334
349
|
|
|
335
350
|
Returns:
|
|
351
|
+
-------
|
|
336
352
|
Created Task instance
|
|
337
353
|
|
|
338
354
|
"""
|
|
@@ -362,13 +378,16 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
362
378
|
"""Update a task or epic.
|
|
363
379
|
|
|
364
380
|
Args:
|
|
381
|
+
----
|
|
365
382
|
ticket_id: ID of ticket to update
|
|
366
383
|
updates: Dictionary of updates or Task object with new values
|
|
367
384
|
|
|
368
385
|
Returns:
|
|
386
|
+
-------
|
|
369
387
|
Updated Task or Epic, or None if ticket not found
|
|
370
388
|
|
|
371
389
|
Raises:
|
|
390
|
+
------
|
|
372
391
|
AttributeError: If update fails due to invalid fields
|
|
373
392
|
|
|
374
393
|
"""
|
|
@@ -555,9 +574,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
555
574
|
"""Get epic by ID.
|
|
556
575
|
|
|
557
576
|
Args:
|
|
577
|
+
----
|
|
558
578
|
epic_id: Epic ID to retrieve
|
|
559
579
|
|
|
560
580
|
Returns:
|
|
581
|
+
-------
|
|
561
582
|
Epic if found, None otherwise
|
|
562
583
|
|
|
563
584
|
"""
|
|
@@ -580,10 +601,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
580
601
|
"""List all epics.
|
|
581
602
|
|
|
582
603
|
Args:
|
|
604
|
+
----
|
|
583
605
|
limit: Maximum number of epics to return
|
|
584
606
|
offset: Number of epics to skip
|
|
585
607
|
|
|
586
608
|
Returns:
|
|
609
|
+
-------
|
|
587
610
|
List of epics
|
|
588
611
|
|
|
589
612
|
"""
|
|
@@ -598,9 +621,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
598
621
|
"""List all issues belonging to an epic.
|
|
599
622
|
|
|
600
623
|
Args:
|
|
624
|
+
----
|
|
601
625
|
epic_id: Epic ID to get issues for
|
|
602
626
|
|
|
603
627
|
Returns:
|
|
628
|
+
-------
|
|
604
629
|
List of issues (tasks with parent_epic set)
|
|
605
630
|
|
|
606
631
|
"""
|
|
@@ -615,9 +640,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
615
640
|
"""List all tasks belonging to an issue.
|
|
616
641
|
|
|
617
642
|
Args:
|
|
643
|
+
----
|
|
618
644
|
issue_id: Issue ID (parent task) to get child tasks for
|
|
619
645
|
|
|
620
646
|
Returns:
|
|
647
|
+
-------
|
|
621
648
|
List of tasks
|
|
622
649
|
|
|
623
650
|
"""
|
|
@@ -633,9 +660,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
633
660
|
"""Sanitize filename to prevent security issues.
|
|
634
661
|
|
|
635
662
|
Args:
|
|
663
|
+
----
|
|
636
664
|
filename: Original filename
|
|
637
665
|
|
|
638
666
|
Returns:
|
|
667
|
+
-------
|
|
639
668
|
Sanitized filename safe for filesystem
|
|
640
669
|
|
|
641
670
|
"""
|
|
@@ -655,9 +684,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
655
684
|
"""Guess MIME type from file extension.
|
|
656
685
|
|
|
657
686
|
Args:
|
|
687
|
+
----
|
|
658
688
|
file_path: Path to file
|
|
659
689
|
|
|
660
690
|
Returns:
|
|
691
|
+
-------
|
|
661
692
|
MIME type string
|
|
662
693
|
|
|
663
694
|
"""
|
|
@@ -670,9 +701,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
670
701
|
"""Calculate SHA256 checksum of file.
|
|
671
702
|
|
|
672
703
|
Args:
|
|
704
|
+
----
|
|
673
705
|
file_path: Path to file
|
|
674
706
|
|
|
675
707
|
Returns:
|
|
708
|
+
-------
|
|
676
709
|
Hexadecimal checksum string
|
|
677
710
|
|
|
678
711
|
"""
|
|
@@ -695,14 +728,17 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
695
728
|
"""Attach a file to a ticket (local filesystem storage).
|
|
696
729
|
|
|
697
730
|
Args:
|
|
731
|
+
----
|
|
698
732
|
ticket_id: Ticket identifier
|
|
699
733
|
file_path: Local file path to attach
|
|
700
734
|
description: Optional attachment description
|
|
701
735
|
|
|
702
736
|
Returns:
|
|
737
|
+
-------
|
|
703
738
|
Attachment metadata
|
|
704
739
|
|
|
705
740
|
Raises:
|
|
741
|
+
------
|
|
706
742
|
ValueError: If ticket doesn't exist
|
|
707
743
|
FileNotFoundError: If file doesn't exist
|
|
708
744
|
|
|
@@ -767,9 +803,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
767
803
|
"""Get all attachments for a ticket with path traversal protection.
|
|
768
804
|
|
|
769
805
|
Args:
|
|
806
|
+
----
|
|
770
807
|
ticket_id: Ticket identifier
|
|
771
808
|
|
|
772
809
|
Returns:
|
|
810
|
+
-------
|
|
773
811
|
List of attachments (empty if none)
|
|
774
812
|
|
|
775
813
|
"""
|
|
@@ -820,10 +858,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
820
858
|
"""Delete an attachment and its metadata with path traversal protection.
|
|
821
859
|
|
|
822
860
|
Args:
|
|
861
|
+
----
|
|
823
862
|
ticket_id: Ticket identifier
|
|
824
863
|
attachment_id: Attachment identifier
|
|
825
864
|
|
|
826
865
|
Returns:
|
|
866
|
+
-------
|
|
827
867
|
True if deleted, False if not found
|
|
828
868
|
|
|
829
869
|
"""
|
|
@@ -861,6 +901,467 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
861
901
|
|
|
862
902
|
return deleted
|
|
863
903
|
|
|
904
|
+
async def update_epic(self, epic_id: str, updates: dict[str, Any]) -> Epic | None:
|
|
905
|
+
"""Update an epic (project) in AITrackdown.
|
|
906
|
+
|
|
907
|
+
Args:
|
|
908
|
+
----
|
|
909
|
+
epic_id: Epic identifier (filename without .json)
|
|
910
|
+
updates: Dictionary of fields to update. Supported fields:
|
|
911
|
+
- title: Epic title
|
|
912
|
+
- description: Epic description
|
|
913
|
+
- state: TicketState value
|
|
914
|
+
- priority: Priority value
|
|
915
|
+
- tags: List of tags
|
|
916
|
+
- target_date: Target completion date
|
|
917
|
+
- metadata: User metadata dictionary
|
|
918
|
+
|
|
919
|
+
Returns:
|
|
920
|
+
-------
|
|
921
|
+
Updated Epic object or None if epic not found
|
|
922
|
+
|
|
923
|
+
Raises:
|
|
924
|
+
------
|
|
925
|
+
ValueError: If epic_id is invalid or epic not found
|
|
926
|
+
|
|
927
|
+
Note:
|
|
928
|
+
----
|
|
929
|
+
AITrackdown stores epics as JSON files in {storage_path}/tickets/
|
|
930
|
+
Updates are applied as partial updates (only specified fields changed)
|
|
931
|
+
|
|
932
|
+
"""
|
|
933
|
+
# Validate epic_id
|
|
934
|
+
if not epic_id:
|
|
935
|
+
raise ValueError("epic_id is required")
|
|
936
|
+
|
|
937
|
+
# Read existing epic
|
|
938
|
+
existing = await self.read(epic_id)
|
|
939
|
+
if not existing:
|
|
940
|
+
logger.warning("Epic %s not found for update", epic_id)
|
|
941
|
+
return None
|
|
942
|
+
|
|
943
|
+
# Ensure it's an epic, not a task
|
|
944
|
+
if not isinstance(existing, Epic):
|
|
945
|
+
logger.warning("Ticket %s is not an epic", epic_id)
|
|
946
|
+
return None
|
|
947
|
+
|
|
948
|
+
# Apply updates to the existing epic
|
|
949
|
+
for key, value in updates.items():
|
|
950
|
+
if hasattr(existing, key) and value is not None:
|
|
951
|
+
setattr(existing, key, value)
|
|
952
|
+
|
|
953
|
+
# Update timestamp
|
|
954
|
+
existing.updated_at = datetime.now()
|
|
955
|
+
|
|
956
|
+
# Write back to file
|
|
957
|
+
ai_ticket = self._epic_to_ai_ticket(existing)
|
|
958
|
+
self._write_ticket_file(epic_id, ai_ticket)
|
|
959
|
+
|
|
960
|
+
logger.info("Updated epic %s with fields: %s", epic_id, list(updates.keys()))
|
|
961
|
+
return existing
|
|
962
|
+
|
|
963
|
+
async def list_labels(self, limit: int = 100) -> builtins.list[dict[str, Any]]:
|
|
964
|
+
"""List all tags (labels) used across tickets.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
----
|
|
968
|
+
limit: Maximum number of labels to return (default: 100)
|
|
969
|
+
|
|
970
|
+
Returns:
|
|
971
|
+
-------
|
|
972
|
+
List of label dictionaries sorted by usage count (descending).
|
|
973
|
+
Each dictionary contains:
|
|
974
|
+
- id: Tag name (same as name in AITrackdown)
|
|
975
|
+
- name: Tag name
|
|
976
|
+
- count: Number of tickets using this tag
|
|
977
|
+
|
|
978
|
+
Note:
|
|
979
|
+
----
|
|
980
|
+
AITrackdown uses 'tags' terminology. This method scans
|
|
981
|
+
all task and epic files to extract unique tags.
|
|
982
|
+
|
|
983
|
+
"""
|
|
984
|
+
# Initialize tag counter
|
|
985
|
+
tag_counts: dict[str, int] = {}
|
|
986
|
+
|
|
987
|
+
# Scan all ticket JSON files
|
|
988
|
+
if self.tickets_dir.exists():
|
|
989
|
+
for ticket_file in self.tickets_dir.glob("*.json"):
|
|
990
|
+
try:
|
|
991
|
+
with open(ticket_file) as f:
|
|
992
|
+
ticket_data = json.load(f)
|
|
993
|
+
tags = ticket_data.get("tags", [])
|
|
994
|
+
for tag in tags:
|
|
995
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
996
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
997
|
+
logger.warning("Failed to read ticket file %s: %s", ticket_file, e)
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
# Sort by usage count (descending)
|
|
1001
|
+
sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
|
1002
|
+
|
|
1003
|
+
# Return top N tags with standardized format
|
|
1004
|
+
return [
|
|
1005
|
+
{"id": tag, "name": tag, "count": count}
|
|
1006
|
+
for tag, count in sorted_tags[:limit]
|
|
1007
|
+
]
|
|
1008
|
+
|
|
1009
|
+
async def create_issue_label(
|
|
1010
|
+
self, name: str, color: str | None = None
|
|
1011
|
+
) -> dict[str, Any]:
|
|
1012
|
+
"""Create/register a label (tag) in AITrackdown.
|
|
1013
|
+
|
|
1014
|
+
Args:
|
|
1015
|
+
----
|
|
1016
|
+
name: Label name (alphanumeric, hyphens, underscores allowed)
|
|
1017
|
+
color: Optional color (not used in file-based storage)
|
|
1018
|
+
|
|
1019
|
+
Returns:
|
|
1020
|
+
-------
|
|
1021
|
+
Label dictionary with:
|
|
1022
|
+
- id: Label name
|
|
1023
|
+
- name: Label name
|
|
1024
|
+
- color: Color value (if provided)
|
|
1025
|
+
- created: True (always, as tags are created on use)
|
|
1026
|
+
|
|
1027
|
+
Raises:
|
|
1028
|
+
------
|
|
1029
|
+
ValueError: If label name is invalid
|
|
1030
|
+
|
|
1031
|
+
Note:
|
|
1032
|
+
----
|
|
1033
|
+
AITrackdown creates tags implicitly when used on tickets.
|
|
1034
|
+
This method validates the tag name and returns success.
|
|
1035
|
+
Tags are stored as arrays in ticket JSON files.
|
|
1036
|
+
|
|
1037
|
+
"""
|
|
1038
|
+
# Validate tag name
|
|
1039
|
+
if not name:
|
|
1040
|
+
raise ValueError("Label name cannot be empty")
|
|
1041
|
+
|
|
1042
|
+
# Check for valid characters (alphanumeric, hyphens, underscores, spaces)
|
|
1043
|
+
import re
|
|
1044
|
+
|
|
1045
|
+
if not re.match(r"^[a-zA-Z0-9_\- ]+$", name):
|
|
1046
|
+
raise ValueError(
|
|
1047
|
+
"Label name must contain only alphanumeric characters, hyphens, underscores, or spaces"
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Return success response
|
|
1051
|
+
logger.info("Label '%s' registered (created implicitly on use)", name)
|
|
1052
|
+
return {
|
|
1053
|
+
"id": name,
|
|
1054
|
+
"name": name,
|
|
1055
|
+
"color": color,
|
|
1056
|
+
"created": True,
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async def list_project_labels(
|
|
1060
|
+
self, epic_id: str, limit: int = 100
|
|
1061
|
+
) -> builtins.list[dict[str, Any]]:
|
|
1062
|
+
"""List labels (tags) used in a specific epic and its tasks.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
----
|
|
1066
|
+
epic_id: Epic identifier
|
|
1067
|
+
limit: Maximum number of labels to return (default: 100)
|
|
1068
|
+
|
|
1069
|
+
Returns:
|
|
1070
|
+
-------
|
|
1071
|
+
List of label dictionaries used in the epic, sorted by usage count.
|
|
1072
|
+
Each dictionary contains:
|
|
1073
|
+
- id: Tag name
|
|
1074
|
+
- name: Tag name
|
|
1075
|
+
- count: Number of tickets using this tag within the epic
|
|
1076
|
+
|
|
1077
|
+
Raises:
|
|
1078
|
+
------
|
|
1079
|
+
ValueError: If epic not found
|
|
1080
|
+
|
|
1081
|
+
Note:
|
|
1082
|
+
----
|
|
1083
|
+
Scans the epic and all tasks with parent_epic == epic_id.
|
|
1084
|
+
|
|
1085
|
+
"""
|
|
1086
|
+
# Validate epic exists
|
|
1087
|
+
epic = await self.get_epic(epic_id)
|
|
1088
|
+
if not epic:
|
|
1089
|
+
raise ValueError(f"Epic {epic_id} not found")
|
|
1090
|
+
|
|
1091
|
+
# Initialize tag counter
|
|
1092
|
+
tag_counts: dict[str, int] = {}
|
|
1093
|
+
|
|
1094
|
+
# Add tags from the epic itself
|
|
1095
|
+
if epic.tags:
|
|
1096
|
+
for tag in epic.tags:
|
|
1097
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
1098
|
+
|
|
1099
|
+
# Find all tasks with parent_epic == epic_id
|
|
1100
|
+
all_tasks = await self.list_issues_by_epic(epic_id)
|
|
1101
|
+
for task in all_tasks:
|
|
1102
|
+
if task.tags:
|
|
1103
|
+
for tag in task.tags:
|
|
1104
|
+
tag_counts[tag] = tag_counts.get(tag, 0) + 1
|
|
1105
|
+
|
|
1106
|
+
# Sort by usage count (descending)
|
|
1107
|
+
sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
|
|
1108
|
+
|
|
1109
|
+
# Return top N tags
|
|
1110
|
+
return [
|
|
1111
|
+
{"id": tag, "name": tag, "count": count}
|
|
1112
|
+
for tag, count in sorted_tags[:limit]
|
|
1113
|
+
]
|
|
1114
|
+
|
|
1115
|
+
async def list_cycles(self, limit: int = 50) -> builtins.list[dict[str, Any]]:
|
|
1116
|
+
"""List cycles (sprints) - Not supported in file-based AITrackdown.
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
----
|
|
1120
|
+
limit: Maximum number of cycles to return (unused)
|
|
1121
|
+
|
|
1122
|
+
Returns:
|
|
1123
|
+
-------
|
|
1124
|
+
Empty list (cycles not supported)
|
|
1125
|
+
|
|
1126
|
+
Note:
|
|
1127
|
+
----
|
|
1128
|
+
AITrackdown is a simple file-based system without
|
|
1129
|
+
cycle/sprint management. Returns empty list.
|
|
1130
|
+
|
|
1131
|
+
"""
|
|
1132
|
+
logger.info("list_cycles called but cycles not supported in AITrackdown")
|
|
1133
|
+
return []
|
|
1134
|
+
|
|
1135
|
+
async def get_issue_status(self, ticket_id: str) -> dict[str, Any] | None:
|
|
1136
|
+
"""Get status details for a ticket.
|
|
1137
|
+
|
|
1138
|
+
Args:
|
|
1139
|
+
----
|
|
1140
|
+
ticket_id: Ticket identifier
|
|
1141
|
+
|
|
1142
|
+
Returns:
|
|
1143
|
+
-------
|
|
1144
|
+
Status dictionary with:
|
|
1145
|
+
- id: Ticket ID
|
|
1146
|
+
- state: Current state
|
|
1147
|
+
- priority: Current priority
|
|
1148
|
+
- updated_at: Last update timestamp
|
|
1149
|
+
- created_at: Creation timestamp
|
|
1150
|
+
- title: Ticket title
|
|
1151
|
+
- assignee: Assignee (if Task, None for Epic)
|
|
1152
|
+
Returns None if ticket not found
|
|
1153
|
+
|
|
1154
|
+
Raises:
|
|
1155
|
+
------
|
|
1156
|
+
ValueError: If ticket_id is invalid
|
|
1157
|
+
|
|
1158
|
+
"""
|
|
1159
|
+
if not ticket_id:
|
|
1160
|
+
raise ValueError("ticket_id is required")
|
|
1161
|
+
|
|
1162
|
+
# Read ticket
|
|
1163
|
+
ticket = await self.read(ticket_id)
|
|
1164
|
+
if not ticket:
|
|
1165
|
+
logger.warning("Ticket %s not found", ticket_id)
|
|
1166
|
+
return None
|
|
1167
|
+
|
|
1168
|
+
# Return comprehensive status object
|
|
1169
|
+
status = {
|
|
1170
|
+
"id": ticket.id,
|
|
1171
|
+
"state": ticket.state,
|
|
1172
|
+
"priority": ticket.priority,
|
|
1173
|
+
"updated_at": ticket.updated_at.isoformat() if ticket.updated_at else None,
|
|
1174
|
+
"created_at": ticket.created_at.isoformat() if ticket.created_at else None,
|
|
1175
|
+
"title": ticket.title,
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
# Add assignee only if ticket is a Task (Epic doesn't have assignee)
|
|
1179
|
+
if hasattr(ticket, "assignee"):
|
|
1180
|
+
status["assignee"] = ticket.assignee
|
|
1181
|
+
|
|
1182
|
+
return status
|
|
1183
|
+
|
|
1184
|
+
async def list_issue_statuses(self) -> builtins.list[dict[str, Any]]:
|
|
1185
|
+
"""List available ticket statuses.
|
|
1186
|
+
|
|
1187
|
+
Returns:
|
|
1188
|
+
-------
|
|
1189
|
+
List of status dictionaries with:
|
|
1190
|
+
- id: State identifier
|
|
1191
|
+
- name: Human-readable state name
|
|
1192
|
+
- description: State description
|
|
1193
|
+
|
|
1194
|
+
Note:
|
|
1195
|
+
----
|
|
1196
|
+
AITrackdown uses standard TicketState enum values:
|
|
1197
|
+
open, in_progress, ready, tested, done, closed, waiting, blocked
|
|
1198
|
+
|
|
1199
|
+
"""
|
|
1200
|
+
# Return hardcoded list of TicketState values
|
|
1201
|
+
statuses = [
|
|
1202
|
+
{
|
|
1203
|
+
"id": "open",
|
|
1204
|
+
"name": "Open",
|
|
1205
|
+
"description": "Ticket is created and ready to be worked on",
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
"id": "in_progress",
|
|
1209
|
+
"name": "In Progress",
|
|
1210
|
+
"description": "Ticket is actively being worked on",
|
|
1211
|
+
},
|
|
1212
|
+
{
|
|
1213
|
+
"id": "ready",
|
|
1214
|
+
"name": "Ready",
|
|
1215
|
+
"description": "Ticket is ready for review or testing",
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
"id": "tested",
|
|
1219
|
+
"name": "Tested",
|
|
1220
|
+
"description": "Ticket has been tested and verified",
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
"id": "done",
|
|
1224
|
+
"name": "Done",
|
|
1225
|
+
"description": "Ticket work is completed",
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
"id": "closed",
|
|
1229
|
+
"name": "Closed",
|
|
1230
|
+
"description": "Ticket is closed and archived",
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
"id": "waiting",
|
|
1234
|
+
"name": "Waiting",
|
|
1235
|
+
"description": "Ticket is waiting for external dependency",
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
"id": "blocked",
|
|
1239
|
+
"name": "Blocked",
|
|
1240
|
+
"description": "Ticket is blocked by an issue or dependency",
|
|
1241
|
+
},
|
|
1242
|
+
]
|
|
1243
|
+
return statuses
|
|
1244
|
+
|
|
1245
|
+
# Milestone Methods (Not yet implemented)
|
|
1246
|
+
|
|
1247
|
+
async def milestone_create(
|
|
1248
|
+
self,
|
|
1249
|
+
name: str,
|
|
1250
|
+
target_date: datetime | None = None,
|
|
1251
|
+
labels: list[str] | None = None,
|
|
1252
|
+
description: str = "",
|
|
1253
|
+
project_id: str | None = None,
|
|
1254
|
+
) -> Any:
|
|
1255
|
+
"""Create milestone - not yet implemented for AITrackdown.
|
|
1256
|
+
|
|
1257
|
+
Args:
|
|
1258
|
+
----
|
|
1259
|
+
name: Milestone name
|
|
1260
|
+
target_date: Target completion date
|
|
1261
|
+
labels: Labels that define this milestone
|
|
1262
|
+
description: Milestone description
|
|
1263
|
+
project_id: Associated project ID
|
|
1264
|
+
|
|
1265
|
+
Raises:
|
|
1266
|
+
------
|
|
1267
|
+
NotImplementedError: Milestone support coming in v2.1.0
|
|
1268
|
+
|
|
1269
|
+
"""
|
|
1270
|
+
raise NotImplementedError("Milestone support for AITrackdown coming in v2.1.0")
|
|
1271
|
+
|
|
1272
|
+
async def milestone_get(self, milestone_id: str) -> Any:
|
|
1273
|
+
"""Get milestone - not yet implemented for AITrackdown.
|
|
1274
|
+
|
|
1275
|
+
Args:
|
|
1276
|
+
----
|
|
1277
|
+
milestone_id: Milestone identifier
|
|
1278
|
+
|
|
1279
|
+
Raises:
|
|
1280
|
+
------
|
|
1281
|
+
NotImplementedError: Milestone support coming in v2.1.0
|
|
1282
|
+
|
|
1283
|
+
"""
|
|
1284
|
+
raise NotImplementedError("Milestone support for AITrackdown coming in v2.1.0")
|
|
1285
|
+
|
|
1286
|
+
async def milestone_list(
|
|
1287
|
+
self,
|
|
1288
|
+
project_id: str | None = None,
|
|
1289
|
+
state: str | None = None,
|
|
1290
|
+
) -> list[Any]:
|
|
1291
|
+
"""List milestones - not yet implemented for AITrackdown.
|
|
1292
|
+
|
|
1293
|
+
Args:
|
|
1294
|
+
----
|
|
1295
|
+
project_id: Filter by project
|
|
1296
|
+
state: Filter by state
|
|
1297
|
+
|
|
1298
|
+
Raises:
|
|
1299
|
+
------
|
|
1300
|
+
NotImplementedError: Milestone support coming in v2.1.0
|
|
1301
|
+
|
|
1302
|
+
"""
|
|
1303
|
+
raise NotImplementedError("Milestone support for AITrackdown coming in v2.1.0")
|
|
1304
|
+
|
|
1305
|
+
async def milestone_update(
|
|
1306
|
+
self,
|
|
1307
|
+
milestone_id: str,
|
|
1308
|
+
name: str | None = None,
|
|
1309
|
+
target_date: datetime | None = None,
|
|
1310
|
+
state: str | None = None,
|
|
1311
|
+
labels: list[str] | None = None,
|
|
1312
|
+
description: str | None = None,
|
|
1313
|
+
) -> Any:
|
|
1314
|
+
"""Update milestone - not yet implemented for AITrackdown.
|
|
1315
|
+
|
|
1316
|
+
Args:
|
|
1317
|
+
----
|
|
1318
|
+
milestone_id: Milestone identifier
|
|
1319
|
+
name: New name
|
|
1320
|
+
target_date: New target date
|
|
1321
|
+
state: New state
|
|
1322
|
+
labels: New labels
|
|
1323
|
+
description: New description
|
|
1324
|
+
|
|
1325
|
+
Raises:
|
|
1326
|
+
------
|
|
1327
|
+
NotImplementedError: Milestone support coming in v2.1.0
|
|
1328
|
+
|
|
1329
|
+
"""
|
|
1330
|
+
raise NotImplementedError("Milestone support for AITrackdown coming in v2.1.0")
|
|
1331
|
+
|
|
1332
|
+
async def milestone_delete(self, milestone_id: str) -> bool:
|
|
1333
|
+
"""Delete milestone - not yet implemented for AITrackdown.
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
----
|
|
1337
|
+
milestone_id: Milestone identifier
|
|
1338
|
+
|
|
1339
|
+
Raises:
|
|
1340
|
+
------
|
|
1341
|
+
NotImplementedError: Milestone support coming in v2.1.0
|
|
1342
|
+
|
|
1343
|
+
"""
|
|
1344
|
+
raise NotImplementedError("Milestone support for AITrackdown coming in v2.1.0")
|
|
1345
|
+
|
|
1346
|
+
async def milestone_get_issues(
|
|
1347
|
+
self,
|
|
1348
|
+
milestone_id: str,
|
|
1349
|
+
state: str | None = None,
|
|
1350
|
+
) -> list[Any]:
|
|
1351
|
+
"""Get milestone issues - not yet implemented for AITrackdown.
|
|
1352
|
+
|
|
1353
|
+
Args:
|
|
1354
|
+
----
|
|
1355
|
+
milestone_id: Milestone identifier
|
|
1356
|
+
state: Filter by issue state
|
|
1357
|
+
|
|
1358
|
+
Raises:
|
|
1359
|
+
------
|
|
1360
|
+
NotImplementedError: Milestone support coming in v2.1.0
|
|
1361
|
+
|
|
1362
|
+
"""
|
|
1363
|
+
raise NotImplementedError("Milestone support for AITrackdown coming in v2.1.0")
|
|
1364
|
+
|
|
864
1365
|
|
|
865
1366
|
# Register the adapter
|
|
866
1367
|
AdapterRegistry.register("aitrackdown", AITrackdownAdapter)
|