mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +394 -9
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +836 -105
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +772 -1
- mcp_ticketer/adapters/linear/adapter.py +2293 -108
- mcp_ticketer/adapters/linear/client.py +146 -12
- mcp_ticketer/adapters/linear/mappers.py +105 -11
- 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/cache/memory.py +3 -3
- mcp_ticketer/cli/adapter_diagnostics.py +4 -2
- mcp_ticketer/cli/auggie_configure.py +18 -6
- mcp_ticketer/cli/codex_configure.py +175 -60
- mcp_ticketer/cli/configure.py +884 -146
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +31 -28
- mcp_ticketer/cli/discover.py +293 -21
- mcp_ticketer/cli/gemini_configure.py +18 -6
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +99 -15
- mcp_ticketer/cli/main.py +109 -2055
- mcp_ticketer/cli/mcp_configure.py +673 -99
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +6 -6
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +13 -11
- mcp_ticketer/cli/ticket_commands.py +277 -36
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +45 -41
- mcp_ticketer/core/__init__.py +35 -1
- mcp_ticketer/core/adapter.py +170 -5
- mcp_ticketer/core/config.py +38 -31
- mcp_ticketer/core/env_discovery.py +33 -3
- mcp_ticketer/core/env_loader.py +7 -6
- mcp_ticketer/core/exceptions.py +10 -4
- mcp_ticketer/core/http_client.py +10 -10
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +32 -20
- mcp_ticketer/core/models.py +136 -1
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +148 -14
- mcp_ticketer/core/registry.py +1 -1
- 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/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +2 -2
- mcp_ticketer/mcp/server/__init__.py +2 -2
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +187 -93
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +37 -9
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
- 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 +1429 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
- mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
- 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 +1182 -82
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/health_monitor.py +1 -0
- mcp_ticketer/queue/manager.py +4 -4
- mcp_ticketer/queue/queue.py +3 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +2 -2
- mcp_ticketer/queue/worker.py +15 -13
- 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.4.11.dist-info/METADATA +0 -496
- mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.11.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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Version information for mcp-ticketer package."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
3
|
+
__version__ = "2.0.1"
|
|
4
4
|
__version_info__ = tuple(int(part) for part in __version__.split("."))
|
|
5
5
|
|
|
6
6
|
# Package metadata
|
|
@@ -27,7 +27,7 @@ __features__ = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def get_version():
|
|
30
|
+
def get_version() -> str:
|
|
31
31
|
"""Return the full version string with build metadata if available."""
|
|
32
32
|
version = __version__
|
|
33
33
|
if __build__:
|
|
@@ -37,6 +37,6 @@ def get_version():
|
|
|
37
37
|
return version
|
|
38
38
|
|
|
39
39
|
|
|
40
|
-
def get_user_agent():
|
|
40
|
+
def get_user_agent() -> str:
|
|
41
41
|
"""Return a user agent string for API requests."""
|
|
42
42
|
return f"{__title__}/{__version__}"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Adapter implementations for various ticket systems."""
|
|
2
2
|
|
|
3
3
|
from .aitrackdown import AITrackdownAdapter
|
|
4
|
+
from .asana import AsanaAdapter
|
|
4
5
|
from .github import GitHubAdapter
|
|
5
6
|
from .hybrid import HybridAdapter
|
|
6
7
|
from .jira import JiraAdapter
|
|
@@ -8,6 +9,7 @@ from .linear import LinearAdapter
|
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
10
11
|
"AITrackdownAdapter",
|
|
12
|
+
"AsanaAdapter",
|
|
11
13
|
"LinearAdapter",
|
|
12
14
|
"JiraAdapter",
|
|
13
15
|
"GitHubAdapter",
|
|
@@ -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,
|
|
@@ -280,15 +288,19 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
280
288
|
|
|
281
289
|
return ticket
|
|
282
290
|
|
|
283
|
-
async def create_epic(
|
|
291
|
+
async def create_epic(
|
|
292
|
+
self, title: str, description: str = None, **kwargs: Any
|
|
293
|
+
) -> Epic:
|
|
284
294
|
"""Create a new epic.
|
|
285
295
|
|
|
286
296
|
Args:
|
|
297
|
+
----
|
|
287
298
|
title: Epic title
|
|
288
299
|
description: Epic description
|
|
289
300
|
**kwargs: Additional epic properties
|
|
290
301
|
|
|
291
302
|
Returns:
|
|
303
|
+
-------
|
|
292
304
|
Created Epic instance
|
|
293
305
|
|
|
294
306
|
"""
|
|
@@ -296,17 +308,23 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
296
308
|
return await self.create(epic)
|
|
297
309
|
|
|
298
310
|
async def create_issue(
|
|
299
|
-
self,
|
|
311
|
+
self,
|
|
312
|
+
title: str,
|
|
313
|
+
parent_epic: str = None,
|
|
314
|
+
description: str = None,
|
|
315
|
+
**kwargs: Any,
|
|
300
316
|
) -> Task:
|
|
301
317
|
"""Create a new issue.
|
|
302
318
|
|
|
303
319
|
Args:
|
|
320
|
+
----
|
|
304
321
|
title: Issue title
|
|
305
322
|
parent_epic: Parent epic ID
|
|
306
323
|
description: Issue description
|
|
307
324
|
**kwargs: Additional issue properties
|
|
308
325
|
|
|
309
326
|
Returns:
|
|
327
|
+
-------
|
|
310
328
|
Created Task instance (representing an issue)
|
|
311
329
|
|
|
312
330
|
"""
|
|
@@ -316,17 +334,19 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
316
334
|
return await self.create(task)
|
|
317
335
|
|
|
318
336
|
async def create_task(
|
|
319
|
-
self, title: str, parent_id: str, description: str = None, **kwargs
|
|
337
|
+
self, title: str, parent_id: str, description: str = None, **kwargs: Any
|
|
320
338
|
) -> Task:
|
|
321
339
|
"""Create a new task under an issue.
|
|
322
340
|
|
|
323
341
|
Args:
|
|
342
|
+
----
|
|
324
343
|
title: Task title
|
|
325
344
|
parent_id: Parent issue ID
|
|
326
345
|
description: Task description
|
|
327
346
|
**kwargs: Additional task properties
|
|
328
347
|
|
|
329
348
|
Returns:
|
|
349
|
+
-------
|
|
330
350
|
Created Task instance
|
|
331
351
|
|
|
332
352
|
"""
|
|
@@ -356,13 +376,16 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
356
376
|
"""Update a task or epic.
|
|
357
377
|
|
|
358
378
|
Args:
|
|
379
|
+
----
|
|
359
380
|
ticket_id: ID of ticket to update
|
|
360
381
|
updates: Dictionary of updates or Task object with new values
|
|
361
382
|
|
|
362
383
|
Returns:
|
|
384
|
+
-------
|
|
363
385
|
Updated Task or Epic, or None if ticket not found
|
|
364
386
|
|
|
365
387
|
Raises:
|
|
388
|
+
------
|
|
366
389
|
AttributeError: If update fails due to invalid fields
|
|
367
390
|
|
|
368
391
|
"""
|
|
@@ -549,9 +572,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
549
572
|
"""Get epic by ID.
|
|
550
573
|
|
|
551
574
|
Args:
|
|
575
|
+
----
|
|
552
576
|
epic_id: Epic ID to retrieve
|
|
553
577
|
|
|
554
578
|
Returns:
|
|
579
|
+
-------
|
|
555
580
|
Epic if found, None otherwise
|
|
556
581
|
|
|
557
582
|
"""
|
|
@@ -574,10 +599,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
574
599
|
"""List all epics.
|
|
575
600
|
|
|
576
601
|
Args:
|
|
602
|
+
----
|
|
577
603
|
limit: Maximum number of epics to return
|
|
578
604
|
offset: Number of epics to skip
|
|
579
605
|
|
|
580
606
|
Returns:
|
|
607
|
+
-------
|
|
581
608
|
List of epics
|
|
582
609
|
|
|
583
610
|
"""
|
|
@@ -592,9 +619,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
592
619
|
"""List all issues belonging to an epic.
|
|
593
620
|
|
|
594
621
|
Args:
|
|
622
|
+
----
|
|
595
623
|
epic_id: Epic ID to get issues for
|
|
596
624
|
|
|
597
625
|
Returns:
|
|
626
|
+
-------
|
|
598
627
|
List of issues (tasks with parent_epic set)
|
|
599
628
|
|
|
600
629
|
"""
|
|
@@ -609,9 +638,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
609
638
|
"""List all tasks belonging to an issue.
|
|
610
639
|
|
|
611
640
|
Args:
|
|
641
|
+
----
|
|
612
642
|
issue_id: Issue ID (parent task) to get child tasks for
|
|
613
643
|
|
|
614
644
|
Returns:
|
|
645
|
+
-------
|
|
615
646
|
List of tasks
|
|
616
647
|
|
|
617
648
|
"""
|
|
@@ -627,9 +658,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
627
658
|
"""Sanitize filename to prevent security issues.
|
|
628
659
|
|
|
629
660
|
Args:
|
|
661
|
+
----
|
|
630
662
|
filename: Original filename
|
|
631
663
|
|
|
632
664
|
Returns:
|
|
665
|
+
-------
|
|
633
666
|
Sanitized filename safe for filesystem
|
|
634
667
|
|
|
635
668
|
"""
|
|
@@ -649,9 +682,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
649
682
|
"""Guess MIME type from file extension.
|
|
650
683
|
|
|
651
684
|
Args:
|
|
685
|
+
----
|
|
652
686
|
file_path: Path to file
|
|
653
687
|
|
|
654
688
|
Returns:
|
|
689
|
+
-------
|
|
655
690
|
MIME type string
|
|
656
691
|
|
|
657
692
|
"""
|
|
@@ -664,9 +699,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
664
699
|
"""Calculate SHA256 checksum of file.
|
|
665
700
|
|
|
666
701
|
Args:
|
|
702
|
+
----
|
|
667
703
|
file_path: Path to file
|
|
668
704
|
|
|
669
705
|
Returns:
|
|
706
|
+
-------
|
|
670
707
|
Hexadecimal checksum string
|
|
671
708
|
|
|
672
709
|
"""
|
|
@@ -689,14 +726,17 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
689
726
|
"""Attach a file to a ticket (local filesystem storage).
|
|
690
727
|
|
|
691
728
|
Args:
|
|
729
|
+
----
|
|
692
730
|
ticket_id: Ticket identifier
|
|
693
731
|
file_path: Local file path to attach
|
|
694
732
|
description: Optional attachment description
|
|
695
733
|
|
|
696
734
|
Returns:
|
|
735
|
+
-------
|
|
697
736
|
Attachment metadata
|
|
698
737
|
|
|
699
738
|
Raises:
|
|
739
|
+
------
|
|
700
740
|
ValueError: If ticket doesn't exist
|
|
701
741
|
FileNotFoundError: If file doesn't exist
|
|
702
742
|
|
|
@@ -761,9 +801,11 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
761
801
|
"""Get all attachments for a ticket with path traversal protection.
|
|
762
802
|
|
|
763
803
|
Args:
|
|
804
|
+
----
|
|
764
805
|
ticket_id: Ticket identifier
|
|
765
806
|
|
|
766
807
|
Returns:
|
|
808
|
+
-------
|
|
767
809
|
List of attachments (empty if none)
|
|
768
810
|
|
|
769
811
|
"""
|
|
@@ -814,10 +856,12 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
814
856
|
"""Delete an attachment and its metadata with path traversal protection.
|
|
815
857
|
|
|
816
858
|
Args:
|
|
859
|
+
----
|
|
817
860
|
ticket_id: Ticket identifier
|
|
818
861
|
attachment_id: Attachment identifier
|
|
819
862
|
|
|
820
863
|
Returns:
|
|
864
|
+
-------
|
|
821
865
|
True if deleted, False if not found
|
|
822
866
|
|
|
823
867
|
"""
|
|
@@ -855,6 +899,347 @@ class AITrackdownAdapter(BaseAdapter[Task]):
|
|
|
855
899
|
|
|
856
900
|
return deleted
|
|
857
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
|
+
|
|
858
1243
|
|
|
859
1244
|
# Register the adapter
|
|
860
1245
|
AdapterRegistry.register("aitrackdown", AITrackdownAdapter)
|