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
|
@@ -5,13 +5,14 @@ attachment information. Note that file attachment functionality may not be
|
|
|
5
5
|
available in all adapters.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import mimetypes
|
|
9
|
+
from pathlib import Path
|
|
8
10
|
from typing import Any
|
|
9
11
|
|
|
10
|
-
from ....core.models import Comment
|
|
11
|
-
from ..server_sdk import get_adapter
|
|
12
|
+
from ....core.models import Comment, TicketType
|
|
13
|
+
from ..server_sdk import get_adapter
|
|
12
14
|
|
|
13
15
|
|
|
14
|
-
@mcp.tool()
|
|
15
16
|
async def ticket_attach(
|
|
16
17
|
ticket_id: str,
|
|
17
18
|
file_path: str,
|
|
@@ -34,7 +35,7 @@ async def ticket_attach(
|
|
|
34
35
|
try:
|
|
35
36
|
adapter = get_adapter()
|
|
36
37
|
|
|
37
|
-
# Read ticket to validate it exists
|
|
38
|
+
# Read ticket to validate it exists and determine type
|
|
38
39
|
ticket = await adapter.read(ticket_id)
|
|
39
40
|
if ticket is None:
|
|
40
41
|
return {
|
|
@@ -42,27 +43,72 @@ async def ticket_attach(
|
|
|
42
43
|
"error": f"Ticket {ticket_id} not found",
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
# Check if
|
|
46
|
-
|
|
46
|
+
# Check if file exists
|
|
47
|
+
file_path_obj = Path(file_path)
|
|
48
|
+
if not file_path_obj.exists():
|
|
47
49
|
return {
|
|
48
50
|
"status": "error",
|
|
49
|
-
"error": f"File
|
|
51
|
+
"error": f"File not found: {file_path}",
|
|
50
52
|
"ticket_id": ticket_id,
|
|
51
|
-
"note": "Consider using ticket_comment to add a reference to the file location",
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
# Try Linear-specific upload methods first (most advanced)
|
|
56
|
+
if hasattr(adapter, "upload_file") and hasattr(adapter, "attach_file_to_issue"):
|
|
57
|
+
try:
|
|
58
|
+
# Determine MIME type
|
|
59
|
+
mime_type = mimetypes.guess_type(file_path)[0]
|
|
60
|
+
|
|
61
|
+
# Upload file to Linear's storage
|
|
62
|
+
file_url = await adapter.upload_file(file_path, mime_type)
|
|
63
|
+
|
|
64
|
+
# Determine ticket type and attach accordingly
|
|
65
|
+
ticket_type = getattr(ticket, "ticket_type", None)
|
|
66
|
+
filename = file_path_obj.name
|
|
67
|
+
|
|
68
|
+
if ticket_type == TicketType.EPIC and hasattr(
|
|
69
|
+
adapter, "attach_file_to_epic"
|
|
70
|
+
):
|
|
71
|
+
# Attach to epic (project)
|
|
72
|
+
result = await adapter.attach_file_to_epic(
|
|
73
|
+
epic_id=ticket_id,
|
|
74
|
+
file_url=file_url,
|
|
75
|
+
title=description or filename,
|
|
76
|
+
subtitle=f"Uploaded file: {filename}",
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
# Attach to issue/task
|
|
80
|
+
result = await adapter.attach_file_to_issue(
|
|
81
|
+
issue_id=ticket_id,
|
|
82
|
+
file_url=file_url,
|
|
83
|
+
title=description or filename,
|
|
84
|
+
subtitle=f"Uploaded file: {filename}",
|
|
85
|
+
comment_body=description if description else None,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"status": "completed",
|
|
90
|
+
"ticket_id": ticket_id,
|
|
91
|
+
"method": "linear_native_upload",
|
|
92
|
+
"file_url": file_url,
|
|
93
|
+
"attachment": result,
|
|
94
|
+
}
|
|
95
|
+
except Exception:
|
|
96
|
+
# Fall through to legacy method if Linear-specific upload fails
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
# Try legacy add_attachment method
|
|
100
|
+
if hasattr(adapter, "add_attachment"):
|
|
101
|
+
attachment = await adapter.add_attachment(
|
|
102
|
+
ticket_id=ticket_id, file_path=file_path, description=description
|
|
103
|
+
)
|
|
58
104
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
105
|
+
return {
|
|
106
|
+
"status": "completed",
|
|
107
|
+
"ticket_id": ticket_id,
|
|
108
|
+
"method": "adapter_native",
|
|
109
|
+
"attachment": attachment,
|
|
110
|
+
}
|
|
64
111
|
|
|
65
|
-
except AttributeError:
|
|
66
112
|
# Fallback: Add file reference as comment
|
|
67
113
|
comment_text = f"Attachment: {file_path}"
|
|
68
114
|
if description:
|
|
@@ -98,7 +144,6 @@ async def ticket_attach(
|
|
|
98
144
|
}
|
|
99
145
|
|
|
100
146
|
|
|
101
|
-
@mcp.tool()
|
|
102
147
|
async def ticket_attachments(
|
|
103
148
|
ticket_id: str,
|
|
104
149
|
) -> dict[str, Any]: # Keep as dict for MCP compatibility
|
|
@@ -135,7 +180,7 @@ async def ticket_attachments(
|
|
|
135
180
|
}
|
|
136
181
|
|
|
137
182
|
# Get attachments via adapter
|
|
138
|
-
attachments = await adapter.get_attachments(ticket_id)
|
|
183
|
+
attachments = await adapter.get_attachments(ticket_id)
|
|
139
184
|
|
|
140
185
|
return {
|
|
141
186
|
"status": "completed",
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
This module implements tools for batch operations on tickets to improve
|
|
4
4
|
efficiency when working with multiple items.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- ticket_bulk: Unified interface for all bulk operations (create, update)
|
|
8
|
+
|
|
9
|
+
All tools follow the MCP response pattern:
|
|
10
|
+
{
|
|
11
|
+
"status": "completed" | "error",
|
|
12
|
+
"summary": {"total": N, "created": N, "updated": N, "failed": N},
|
|
13
|
+
"results": {...}
|
|
14
|
+
}
|
|
5
15
|
"""
|
|
6
16
|
|
|
7
17
|
from typing import Any
|
|
@@ -11,263 +21,310 @@ from ..server_sdk import get_adapter, mcp
|
|
|
11
21
|
|
|
12
22
|
|
|
13
23
|
@mcp.tool()
|
|
14
|
-
async def
|
|
15
|
-
|
|
24
|
+
async def ticket_bulk(
|
|
25
|
+
action: str,
|
|
26
|
+
tickets: list[dict[str, Any]] | None = None,
|
|
27
|
+
updates: list[dict[str, Any]] | None = None,
|
|
16
28
|
) -> dict[str, Any]:
|
|
17
|
-
"""
|
|
29
|
+
"""Unified bulk ticket operations tool.
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
Performs bulk create or update operations on tickets through a single
|
|
32
|
+
interface.
|
|
21
33
|
|
|
22
34
|
Args:
|
|
23
|
-
|
|
35
|
+
action: Operation to perform. Valid values:
|
|
36
|
+
- "create": Create multiple new tickets
|
|
37
|
+
- "update": Update multiple existing tickets
|
|
38
|
+
tickets: List of ticket dicts for bulk create (required if action="create")
|
|
39
|
+
Each dict should contain at minimum 'title', with optional fields:
|
|
40
|
+
description, priority, tags, assignee, ticket_type, parent_epic, parent_issue
|
|
41
|
+
updates: List of update dicts for bulk update (required if action="update")
|
|
42
|
+
Each dict must contain 'ticket_id' and at least one field to update.
|
|
43
|
+
Valid update fields: title, description, priority, state, assignee, tags
|
|
24
44
|
|
|
25
45
|
Returns:
|
|
26
|
-
Results
|
|
27
|
-
|
|
46
|
+
Results dictionary containing:
|
|
47
|
+
- status: "completed" or "error"
|
|
48
|
+
- summary: Statistics (total, created/updated, failed)
|
|
49
|
+
- results: Detailed results for each operation
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
ValueError: If action is invalid or required parameters missing
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
# Bulk create
|
|
56
|
+
result = await ticket_bulk(
|
|
57
|
+
action="create",
|
|
58
|
+
tickets=[
|
|
59
|
+
{"title": "Bug 1", "priority": "high", "description": "Fix login"},
|
|
60
|
+
{"title": "Bug 2", "priority": "medium", "tags": ["backend"]}
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Bulk update
|
|
65
|
+
result = await ticket_bulk(
|
|
66
|
+
action="update",
|
|
67
|
+
updates=[
|
|
68
|
+
{"ticket_id": "PROJ-123", "state": "done", "priority": "low"},
|
|
69
|
+
{"ticket_id": "PROJ-456", "assignee": "user@example.com"}
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
See: docs/mcp-api-reference.md for detailed response formats
|
|
28
74
|
"""
|
|
29
|
-
|
|
30
|
-
adapter = get_adapter()
|
|
75
|
+
action_lower = action.lower()
|
|
31
76
|
|
|
32
|
-
|
|
77
|
+
# Route to appropriate handler based on action
|
|
78
|
+
if action_lower == "create":
|
|
79
|
+
if tickets is None:
|
|
33
80
|
return {
|
|
34
81
|
"status": "error",
|
|
35
|
-
"error": "
|
|
82
|
+
"error": "tickets parameter required for action='create'",
|
|
83
|
+
"hint": "Use ticket_bulk(action='create', tickets=[...])",
|
|
84
|
+
}
|
|
85
|
+
# Inline implementation of bulk create
|
|
86
|
+
try:
|
|
87
|
+
adapter = get_adapter()
|
|
88
|
+
|
|
89
|
+
if not tickets:
|
|
90
|
+
return {
|
|
91
|
+
"status": "error",
|
|
92
|
+
"error": "No tickets provided for bulk creation",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
results: dict[str, list[Any]] = {
|
|
96
|
+
"created": [],
|
|
97
|
+
"failed": [],
|
|
36
98
|
}
|
|
37
99
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
for i, ticket_data in enumerate(tickets):
|
|
44
|
-
try:
|
|
45
|
-
# Validate required fields
|
|
46
|
-
if "title" not in ticket_data:
|
|
47
|
-
results["failed"].append(
|
|
48
|
-
{
|
|
49
|
-
"index": i,
|
|
50
|
-
"error": "Missing required field: title",
|
|
51
|
-
"data": ticket_data,
|
|
52
|
-
}
|
|
53
|
-
)
|
|
54
|
-
continue
|
|
55
|
-
|
|
56
|
-
# Parse priority if provided
|
|
57
|
-
priority = Priority.MEDIUM # Default
|
|
58
|
-
if "priority" in ticket_data:
|
|
59
|
-
try:
|
|
60
|
-
priority = Priority(ticket_data["priority"].lower())
|
|
61
|
-
except ValueError:
|
|
62
|
-
results["failed"].append(
|
|
63
|
-
{
|
|
64
|
-
"index": i,
|
|
65
|
-
"error": f"Invalid priority: {ticket_data['priority']}",
|
|
66
|
-
"data": ticket_data,
|
|
67
|
-
}
|
|
68
|
-
)
|
|
69
|
-
continue
|
|
70
|
-
|
|
71
|
-
# Parse ticket type if provided
|
|
72
|
-
ticket_type = TicketType.ISSUE # Default
|
|
73
|
-
if "ticket_type" in ticket_data:
|
|
74
|
-
try:
|
|
75
|
-
ticket_type = TicketType(ticket_data["ticket_type"].lower())
|
|
76
|
-
except ValueError:
|
|
100
|
+
for i, ticket_data in enumerate(tickets):
|
|
101
|
+
try:
|
|
102
|
+
# Validate required fields
|
|
103
|
+
if "title" not in ticket_data:
|
|
77
104
|
results["failed"].append(
|
|
78
105
|
{
|
|
79
106
|
"index": i,
|
|
80
|
-
"error":
|
|
107
|
+
"error": "Missing required field: title",
|
|
81
108
|
"data": ticket_data,
|
|
82
109
|
}
|
|
83
110
|
)
|
|
84
111
|
continue
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
"status": "error",
|
|
129
|
-
"error": f"Bulk creation failed: {str(e)}",
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
@mcp.tool()
|
|
134
|
-
async def ticket_bulk_update(
|
|
135
|
-
updates: list[dict[str, Any]],
|
|
136
|
-
) -> dict[str, Any]:
|
|
137
|
-
"""Update multiple tickets in a single operation.
|
|
138
|
-
|
|
139
|
-
Each update dict must contain 'ticket_id' and at least one field to update.
|
|
140
|
-
Valid update fields: title, description, priority, state, assignee, tags.
|
|
113
|
+
# Parse priority if provided
|
|
114
|
+
priority = Priority.MEDIUM # Default
|
|
115
|
+
if "priority" in ticket_data:
|
|
116
|
+
try:
|
|
117
|
+
priority = Priority(ticket_data["priority"].lower())
|
|
118
|
+
except ValueError:
|
|
119
|
+
results["failed"].append(
|
|
120
|
+
{
|
|
121
|
+
"index": i,
|
|
122
|
+
"error": f"Invalid priority: {ticket_data['priority']}",
|
|
123
|
+
"data": ticket_data,
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Parse ticket type if provided
|
|
129
|
+
ticket_type = TicketType.ISSUE # Default
|
|
130
|
+
if "ticket_type" in ticket_data:
|
|
131
|
+
try:
|
|
132
|
+
ticket_type = TicketType(ticket_data["ticket_type"].lower())
|
|
133
|
+
except ValueError:
|
|
134
|
+
results["failed"].append(
|
|
135
|
+
{
|
|
136
|
+
"index": i,
|
|
137
|
+
"error": f"Invalid ticket_type: {ticket_data['ticket_type']}",
|
|
138
|
+
"data": ticket_data,
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Create task object
|
|
144
|
+
task = Task(
|
|
145
|
+
title=ticket_data["title"],
|
|
146
|
+
description=ticket_data.get("description", ""),
|
|
147
|
+
priority=priority,
|
|
148
|
+
ticket_type=ticket_type,
|
|
149
|
+
tags=ticket_data.get("tags", []),
|
|
150
|
+
assignee=ticket_data.get("assignee"),
|
|
151
|
+
parent_epic=ticket_data.get("parent_epic"),
|
|
152
|
+
parent_issue=ticket_data.get("parent_issue"),
|
|
153
|
+
)
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
# Create via adapter
|
|
156
|
+
created = await adapter.create(task)
|
|
157
|
+
results["created"].append(
|
|
158
|
+
{
|
|
159
|
+
"index": i,
|
|
160
|
+
"ticket": created.model_dump(),
|
|
161
|
+
}
|
|
162
|
+
)
|
|
144
163
|
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
except Exception as e:
|
|
165
|
+
results["failed"].append(
|
|
166
|
+
{
|
|
167
|
+
"index": i,
|
|
168
|
+
"error": str(e),
|
|
169
|
+
"data": ticket_data,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
147
172
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
173
|
+
return {
|
|
174
|
+
"status": "completed",
|
|
175
|
+
"summary": {
|
|
176
|
+
"total": len(tickets),
|
|
177
|
+
"created": len(results["created"]),
|
|
178
|
+
"failed": len(results["failed"]),
|
|
179
|
+
},
|
|
180
|
+
"results": results,
|
|
181
|
+
}
|
|
151
182
|
|
|
152
|
-
|
|
183
|
+
except Exception as e:
|
|
153
184
|
return {
|
|
154
185
|
"status": "error",
|
|
155
|
-
"error": "
|
|
186
|
+
"error": f"Bulk creation failed: {str(e)}",
|
|
156
187
|
}
|
|
157
188
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
189
|
+
elif action_lower == "update":
|
|
190
|
+
if updates is None:
|
|
191
|
+
return {
|
|
192
|
+
"status": "error",
|
|
193
|
+
"error": "updates parameter required for action='update'",
|
|
194
|
+
"hint": "Use ticket_bulk(action='update', updates=[...])",
|
|
195
|
+
}
|
|
196
|
+
# Inline implementation of bulk update
|
|
197
|
+
try:
|
|
198
|
+
adapter = get_adapter()
|
|
199
|
+
|
|
200
|
+
if not updates:
|
|
201
|
+
return {
|
|
202
|
+
"status": "error",
|
|
203
|
+
"error": "No updates provided for bulk operation",
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
results: dict[str, list[Any]] = {
|
|
207
|
+
"updated": [],
|
|
208
|
+
"failed": [],
|
|
209
|
+
}
|
|
162
210
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
results["failed"].append(
|
|
168
|
-
{
|
|
169
|
-
"index": i,
|
|
170
|
-
"error": "Missing required field: ticket_id",
|
|
171
|
-
"data": update_data,
|
|
172
|
-
}
|
|
173
|
-
)
|
|
174
|
-
continue
|
|
175
|
-
|
|
176
|
-
ticket_id = update_data["ticket_id"]
|
|
177
|
-
|
|
178
|
-
# Build update dict
|
|
179
|
-
update_fields: dict[str, Any] = {}
|
|
180
|
-
|
|
181
|
-
if "title" in update_data:
|
|
182
|
-
update_fields["title"] = update_data["title"]
|
|
183
|
-
if "description" in update_data:
|
|
184
|
-
update_fields["description"] = update_data["description"]
|
|
185
|
-
if "assignee" in update_data:
|
|
186
|
-
update_fields["assignee"] = update_data["assignee"]
|
|
187
|
-
if "tags" in update_data:
|
|
188
|
-
update_fields["tags"] = update_data["tags"]
|
|
189
|
-
|
|
190
|
-
# Parse priority if provided
|
|
191
|
-
if "priority" in update_data:
|
|
192
|
-
try:
|
|
193
|
-
update_fields["priority"] = Priority(
|
|
194
|
-
update_data["priority"].lower()
|
|
195
|
-
)
|
|
196
|
-
except ValueError:
|
|
211
|
+
for i, update_data in enumerate(updates):
|
|
212
|
+
try:
|
|
213
|
+
# Validate required fields
|
|
214
|
+
if "ticket_id" not in update_data:
|
|
197
215
|
results["failed"].append(
|
|
198
216
|
{
|
|
199
217
|
"index": i,
|
|
200
|
-
"error":
|
|
218
|
+
"error": "Missing required field: ticket_id",
|
|
201
219
|
"data": update_data,
|
|
202
220
|
}
|
|
203
221
|
)
|
|
204
222
|
continue
|
|
205
223
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
224
|
+
ticket_id = update_data["ticket_id"]
|
|
225
|
+
|
|
226
|
+
# Build update dict
|
|
227
|
+
update_fields: dict[str, Any] = {}
|
|
228
|
+
|
|
229
|
+
if "title" in update_data:
|
|
230
|
+
update_fields["title"] = update_data["title"]
|
|
231
|
+
if "description" in update_data:
|
|
232
|
+
update_fields["description"] = update_data["description"]
|
|
233
|
+
if "assignee" in update_data:
|
|
234
|
+
update_fields["assignee"] = update_data["assignee"]
|
|
235
|
+
if "tags" in update_data:
|
|
236
|
+
update_fields["tags"] = update_data["tags"]
|
|
237
|
+
|
|
238
|
+
# Parse priority if provided
|
|
239
|
+
if "priority" in update_data:
|
|
240
|
+
try:
|
|
241
|
+
update_fields["priority"] = Priority(
|
|
242
|
+
update_data["priority"].lower()
|
|
243
|
+
)
|
|
244
|
+
except ValueError:
|
|
245
|
+
results["failed"].append(
|
|
246
|
+
{
|
|
247
|
+
"index": i,
|
|
248
|
+
"error": f"Invalid priority: {update_data['priority']}",
|
|
249
|
+
"data": update_data,
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Parse state if provided
|
|
255
|
+
if "state" in update_data:
|
|
256
|
+
try:
|
|
257
|
+
update_fields["state"] = TicketState(
|
|
258
|
+
update_data["state"].lower()
|
|
259
|
+
)
|
|
260
|
+
except ValueError:
|
|
261
|
+
results["failed"].append(
|
|
262
|
+
{
|
|
263
|
+
"index": i,
|
|
264
|
+
"error": f"Invalid state: {update_data['state']}",
|
|
265
|
+
"data": update_data,
|
|
266
|
+
}
|
|
267
|
+
)
|
|
268
|
+
continue
|
|
269
|
+
|
|
270
|
+
if not update_fields:
|
|
213
271
|
results["failed"].append(
|
|
214
272
|
{
|
|
215
273
|
"index": i,
|
|
216
|
-
"error":
|
|
274
|
+
"error": "No valid update fields provided",
|
|
217
275
|
"data": update_data,
|
|
218
276
|
}
|
|
219
277
|
)
|
|
220
278
|
continue
|
|
221
279
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
280
|
+
# Update via adapter
|
|
281
|
+
updated = await adapter.update(ticket_id, update_fields)
|
|
282
|
+
if updated is None:
|
|
283
|
+
results["failed"].append(
|
|
284
|
+
{
|
|
285
|
+
"index": i,
|
|
286
|
+
"error": f"Ticket {ticket_id} not found or update failed",
|
|
287
|
+
"data": update_data,
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
results["updated"].append(
|
|
292
|
+
{
|
|
293
|
+
"index": i,
|
|
294
|
+
"ticket": updated.model_dump(),
|
|
295
|
+
}
|
|
296
|
+
)
|
|
231
297
|
|
|
232
|
-
|
|
233
|
-
updated = await adapter.update(ticket_id, update_fields)
|
|
234
|
-
if updated is None:
|
|
298
|
+
except Exception as e:
|
|
235
299
|
results["failed"].append(
|
|
236
300
|
{
|
|
237
301
|
"index": i,
|
|
238
|
-
"error":
|
|
302
|
+
"error": str(e),
|
|
239
303
|
"data": update_data,
|
|
240
304
|
}
|
|
241
305
|
)
|
|
242
|
-
else:
|
|
243
|
-
results["updated"].append(
|
|
244
|
-
{
|
|
245
|
-
"index": i,
|
|
246
|
-
"ticket": updated.model_dump(),
|
|
247
|
-
}
|
|
248
|
-
)
|
|
249
306
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
307
|
+
return {
|
|
308
|
+
"status": "completed",
|
|
309
|
+
"summary": {
|
|
310
|
+
"total": len(updates),
|
|
311
|
+
"updated": len(results["updated"]),
|
|
312
|
+
"failed": len(results["failed"]),
|
|
313
|
+
},
|
|
314
|
+
"results": results,
|
|
315
|
+
}
|
|
258
316
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
"failed": len(results["failed"]),
|
|
265
|
-
},
|
|
266
|
-
"results": results,
|
|
267
|
-
}
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return {
|
|
319
|
+
"status": "error",
|
|
320
|
+
"error": f"Bulk update failed: {str(e)}",
|
|
321
|
+
}
|
|
268
322
|
|
|
269
|
-
|
|
323
|
+
else:
|
|
324
|
+
valid_actions = ["create", "update"]
|
|
270
325
|
return {
|
|
271
326
|
"status": "error",
|
|
272
|
-
"error": f"
|
|
327
|
+
"error": f"Invalid action '{action}'. Must be one of: {', '.join(valid_actions)}",
|
|
328
|
+
"valid_actions": valid_actions,
|
|
329
|
+
"hint": "Use ticket_bulk(action='create'|'update', ...)",
|
|
273
330
|
}
|