mcp-ticketer 0.3.0__py3-none-any.whl → 2.2.9__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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/_version_scm.py +1 -0
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +930 -52
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1537 -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/__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 +58 -16
- 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/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +3810 -462
- mcp_ticketer/adapters/linear/client.py +312 -69
- mcp_ticketer/adapters/linear/mappers.py +305 -85
- mcp_ticketer/adapters/linear/queries.py +317 -17
- mcp_ticketer/adapters/linear/types.py +187 -64
- mcp_ticketer/adapters/linear.py +2 -2
- 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 +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +91 -54
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +1323 -151
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +209 -114
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/install_mcp_server.py +418 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +256 -130
- mcp_ticketer/cli/main.py +140 -1544
- mcp_ticketer/cli/mcp_configure.py +1013 -100
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +477 -0
- mcp_ticketer/cli/platform_installer.py +545 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +794 -0
- mcp_ticketer/cli/simple_health.py +84 -59
- mcp_ticketer/cli/ticket_commands.py +1375 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +195 -72
- mcp_ticketer/core/__init__.py +64 -1
- mcp_ticketer/core/adapter.py +618 -18
- mcp_ticketer/core/config.py +77 -68
- mcp_ticketer/core/env_discovery.py +75 -16
- mcp_ticketer/core/env_loader.py +121 -97
- mcp_ticketer/core/exceptions.py +32 -24
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/milestone_manager.py +252 -0
- mcp_ticketer/core/models.py +566 -19
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +189 -49
- mcp_ticketer/core/project_utils.py +281 -0
- mcp_ticketer/core/project_validator.py +376 -0
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +176 -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 +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +723 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +69 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +224 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +330 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1564 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +942 -0
- 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/milestone_tools.py +338 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +150 -0
- 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 +318 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1413 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +78 -63
- mcp_ticketer/queue/queue.py +108 -21
- mcp_ticketer/queue/run_worker.py +2 -2
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +96 -58
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.2.9.dist-info/METADATA +1396 -0
- mcp_ticketer-2.2.9.dist-info/RECORD +158 -0
- mcp_ticketer-2.2.9.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 -1354
- mcp_ticketer/adapters/jira.py +0 -1011
- mcp_ticketer/mcp/server.py +0 -2030
- mcp_ticketer-0.3.0.dist-info/METADATA +0 -414
- mcp_ticketer-0.3.0.dist-info/RECORD +0 -59
- mcp_ticketer-0.3.0.dist-info/top_level.txt +0 -1
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.0.dist-info → mcp_ticketer-2.2.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,79 +3,89 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import datetime
|
|
6
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
7
7
|
|
|
8
|
-
from ...core.models import Comment, Epic, Priority, Task, TicketState
|
|
9
|
-
from .types import
|
|
10
|
-
extract_linear_metadata,
|
|
11
|
-
get_universal_priority,
|
|
12
|
-
get_universal_state,
|
|
13
|
-
)
|
|
8
|
+
from ...core.models import Attachment, Comment, Epic, Priority, Task, TicketState
|
|
9
|
+
from .types import extract_linear_metadata, get_universal_priority, get_universal_state
|
|
14
10
|
|
|
15
11
|
|
|
16
|
-
def map_linear_issue_to_task(issue_data:
|
|
17
|
-
"""Convert Linear issue data to universal Task model.
|
|
18
|
-
|
|
12
|
+
def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
|
|
13
|
+
"""Convert Linear issue or sub-issue data to universal Task model.
|
|
14
|
+
|
|
15
|
+
Handles both top-level issues (no parent) and sub-issues (child items
|
|
16
|
+
with a parent issue).
|
|
17
|
+
|
|
19
18
|
Args:
|
|
19
|
+
----
|
|
20
20
|
issue_data: Raw Linear issue data from GraphQL
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Returns:
|
|
23
|
+
-------
|
|
23
24
|
Universal Task model
|
|
25
|
+
|
|
24
26
|
"""
|
|
25
27
|
# Extract basic fields
|
|
26
28
|
task_id = issue_data["identifier"]
|
|
27
29
|
title = issue_data["title"]
|
|
28
30
|
description = issue_data.get("description", "")
|
|
29
|
-
|
|
31
|
+
|
|
30
32
|
# Map priority
|
|
31
33
|
linear_priority = issue_data.get("priority", 3)
|
|
32
34
|
priority = get_universal_priority(linear_priority)
|
|
33
|
-
|
|
34
|
-
# Map state
|
|
35
|
+
|
|
36
|
+
# Map state with synonym matching (1M-164)
|
|
35
37
|
state_data = issue_data.get("state", {})
|
|
36
38
|
state_type = state_data.get("type", "unstarted")
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
state_name = state_data.get("name") # Extract state name for synonym matching
|
|
40
|
+
state = get_universal_state(state_type, state_name)
|
|
41
|
+
|
|
39
42
|
# Extract assignee
|
|
40
43
|
assignee = None
|
|
41
44
|
if issue_data.get("assignee"):
|
|
42
45
|
assignee_data = issue_data["assignee"]
|
|
43
46
|
assignee = assignee_data.get("email") or assignee_data.get("displayName")
|
|
44
|
-
|
|
47
|
+
|
|
45
48
|
# Extract creator
|
|
46
49
|
creator = None
|
|
47
50
|
if issue_data.get("creator"):
|
|
48
51
|
creator_data = issue_data["creator"]
|
|
49
52
|
creator = creator_data.get("email") or creator_data.get("displayName")
|
|
50
|
-
|
|
53
|
+
|
|
51
54
|
# Extract tags (labels)
|
|
52
55
|
tags = []
|
|
53
56
|
if issue_data.get("labels", {}).get("nodes"):
|
|
54
57
|
tags = [label["name"] for label in issue_data["labels"]["nodes"]]
|
|
55
|
-
|
|
58
|
+
|
|
56
59
|
# Extract parent epic (project)
|
|
57
60
|
parent_epic = None
|
|
58
61
|
if issue_data.get("project"):
|
|
59
62
|
parent_epic = issue_data["project"]["id"]
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
# Extract parent issue
|
|
62
65
|
parent_issue = None
|
|
63
66
|
if issue_data.get("parent"):
|
|
64
67
|
parent_issue = issue_data["parent"]["identifier"]
|
|
65
|
-
|
|
68
|
+
|
|
66
69
|
# Extract dates
|
|
67
70
|
created_at = None
|
|
68
71
|
if issue_data.get("createdAt"):
|
|
69
|
-
created_at = datetime.fromisoformat(
|
|
70
|
-
|
|
72
|
+
created_at = datetime.fromisoformat(
|
|
73
|
+
issue_data["createdAt"].replace("Z", "+00:00")
|
|
74
|
+
)
|
|
75
|
+
|
|
71
76
|
updated_at = None
|
|
72
77
|
if issue_data.get("updatedAt"):
|
|
73
|
-
updated_at = datetime.fromisoformat(
|
|
74
|
-
|
|
78
|
+
updated_at = datetime.fromisoformat(
|
|
79
|
+
issue_data["updatedAt"].replace("Z", "+00:00")
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Extract child issue IDs
|
|
83
|
+
children = extract_child_issue_ids(issue_data)
|
|
84
|
+
|
|
75
85
|
# Extract Linear-specific metadata
|
|
76
86
|
linear_metadata = extract_linear_metadata(issue_data)
|
|
77
|
-
metadata = {"linear": linear_metadata} if linear_metadata else
|
|
78
|
-
|
|
87
|
+
metadata = {"linear": linear_metadata} if linear_metadata else {}
|
|
88
|
+
|
|
79
89
|
return Task(
|
|
80
90
|
id=task_id,
|
|
81
91
|
title=title,
|
|
@@ -87,26 +97,30 @@ def map_linear_issue_to_task(issue_data: Dict[str, Any]) -> Task:
|
|
|
87
97
|
tags=tags,
|
|
88
98
|
parent_epic=parent_epic,
|
|
89
99
|
parent_issue=parent_issue,
|
|
100
|
+
children=children,
|
|
90
101
|
created_at=created_at,
|
|
91
102
|
updated_at=updated_at,
|
|
92
103
|
metadata=metadata,
|
|
93
104
|
)
|
|
94
105
|
|
|
95
106
|
|
|
96
|
-
def map_linear_project_to_epic(project_data:
|
|
107
|
+
def map_linear_project_to_epic(project_data: dict[str, Any]) -> Epic:
|
|
97
108
|
"""Convert Linear project data to universal Epic model.
|
|
98
|
-
|
|
109
|
+
|
|
99
110
|
Args:
|
|
111
|
+
----
|
|
100
112
|
project_data: Raw Linear project data from GraphQL
|
|
101
|
-
|
|
113
|
+
|
|
102
114
|
Returns:
|
|
115
|
+
-------
|
|
103
116
|
Universal Epic model
|
|
117
|
+
|
|
104
118
|
"""
|
|
105
119
|
# Extract basic fields
|
|
106
120
|
epic_id = project_data["id"]
|
|
107
121
|
title = project_data["name"]
|
|
108
122
|
description = project_data.get("description", "")
|
|
109
|
-
|
|
123
|
+
|
|
110
124
|
# Map state based on project state
|
|
111
125
|
project_state = project_data.get("state", "planned")
|
|
112
126
|
if project_state == "completed":
|
|
@@ -117,18 +131,22 @@ def map_linear_project_to_epic(project_data: Dict[str, Any]) -> Epic:
|
|
|
117
131
|
state = TicketState.CLOSED
|
|
118
132
|
else:
|
|
119
133
|
state = TicketState.OPEN
|
|
120
|
-
|
|
134
|
+
|
|
121
135
|
# Extract dates
|
|
122
136
|
created_at = None
|
|
123
137
|
if project_data.get("createdAt"):
|
|
124
|
-
created_at = datetime.fromisoformat(
|
|
125
|
-
|
|
138
|
+
created_at = datetime.fromisoformat(
|
|
139
|
+
project_data["createdAt"].replace("Z", "+00:00")
|
|
140
|
+
)
|
|
141
|
+
|
|
126
142
|
updated_at = None
|
|
127
143
|
if project_data.get("updatedAt"):
|
|
128
|
-
updated_at = datetime.fromisoformat(
|
|
129
|
-
|
|
144
|
+
updated_at = datetime.fromisoformat(
|
|
145
|
+
project_data["updatedAt"].replace("Z", "+00:00")
|
|
146
|
+
)
|
|
147
|
+
|
|
130
148
|
# Extract Linear-specific metadata
|
|
131
|
-
metadata = {"linear": {}}
|
|
149
|
+
metadata: dict[str, Any] = {"linear": {}}
|
|
132
150
|
if project_data.get("url"):
|
|
133
151
|
metadata["linear"]["linear_url"] = project_data["url"]
|
|
134
152
|
if project_data.get("icon"):
|
|
@@ -137,7 +155,7 @@ def map_linear_project_to_epic(project_data: Dict[str, Any]) -> Epic:
|
|
|
137
155
|
metadata["linear"]["color"] = project_data["color"]
|
|
138
156
|
if project_data.get("targetDate"):
|
|
139
157
|
metadata["linear"]["target_date"] = project_data["targetDate"]
|
|
140
|
-
|
|
158
|
+
|
|
141
159
|
return Epic(
|
|
142
160
|
id=epic_id,
|
|
143
161
|
title=title,
|
|
@@ -146,92 +164,115 @@ def map_linear_project_to_epic(project_data: Dict[str, Any]) -> Epic:
|
|
|
146
164
|
priority=Priority.MEDIUM, # Projects don't have priority in Linear
|
|
147
165
|
created_at=created_at,
|
|
148
166
|
updated_at=updated_at,
|
|
149
|
-
metadata=metadata if metadata["linear"] else
|
|
167
|
+
metadata=metadata if metadata["linear"] else {},
|
|
150
168
|
)
|
|
151
169
|
|
|
152
170
|
|
|
153
|
-
def map_linear_comment_to_comment(
|
|
171
|
+
def map_linear_comment_to_comment(
|
|
172
|
+
comment_data: dict[str, Any], ticket_id: str
|
|
173
|
+
) -> Comment:
|
|
154
174
|
"""Convert Linear comment data to universal Comment model.
|
|
155
|
-
|
|
175
|
+
|
|
156
176
|
Args:
|
|
177
|
+
----
|
|
157
178
|
comment_data: Raw Linear comment data from GraphQL
|
|
158
179
|
ticket_id: ID of the ticket this comment belongs to
|
|
159
|
-
|
|
180
|
+
|
|
160
181
|
Returns:
|
|
182
|
+
-------
|
|
161
183
|
Universal Comment model
|
|
184
|
+
|
|
162
185
|
"""
|
|
163
186
|
# Extract basic fields
|
|
164
187
|
comment_id = comment_data["id"]
|
|
165
188
|
body = comment_data.get("body", "")
|
|
166
|
-
|
|
189
|
+
|
|
167
190
|
# Extract author
|
|
168
191
|
author = None
|
|
169
192
|
if comment_data.get("user"):
|
|
170
193
|
user_data = comment_data["user"]
|
|
171
194
|
author = user_data.get("email") or user_data.get("displayName")
|
|
172
|
-
|
|
195
|
+
|
|
173
196
|
# Extract dates
|
|
174
197
|
created_at = None
|
|
175
198
|
if comment_data.get("createdAt"):
|
|
176
|
-
created_at = datetime.fromisoformat(
|
|
177
|
-
|
|
178
|
-
|
|
199
|
+
created_at = datetime.fromisoformat(
|
|
200
|
+
comment_data["createdAt"].replace("Z", "+00:00")
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Note: Comment model doesn't have updated_at field
|
|
204
|
+
# Store it in metadata if needed
|
|
205
|
+
metadata = {}
|
|
179
206
|
if comment_data.get("updatedAt"):
|
|
180
|
-
updated_at =
|
|
181
|
-
|
|
207
|
+
metadata["updated_at"] = comment_data["updatedAt"]
|
|
208
|
+
|
|
182
209
|
return Comment(
|
|
183
210
|
id=comment_id,
|
|
184
211
|
ticket_id=ticket_id,
|
|
185
|
-
|
|
212
|
+
content=body,
|
|
186
213
|
author=author,
|
|
187
214
|
created_at=created_at,
|
|
188
|
-
|
|
215
|
+
metadata=metadata,
|
|
189
216
|
)
|
|
190
217
|
|
|
191
218
|
|
|
192
|
-
def build_linear_issue_input(task: Task, team_id: str) ->
|
|
193
|
-
"""Build Linear issue input from universal Task model.
|
|
194
|
-
|
|
219
|
+
def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
|
|
220
|
+
"""Build Linear issue or sub-issue input from universal Task model.
|
|
221
|
+
|
|
222
|
+
Creates input for a top-level issue when task.parent_issue is not set,
|
|
223
|
+
or for a sub-issue when task.parent_issue is provided.
|
|
224
|
+
|
|
195
225
|
Args:
|
|
226
|
+
----
|
|
196
227
|
task: Universal Task model
|
|
197
228
|
team_id: Linear team ID
|
|
198
|
-
|
|
229
|
+
|
|
199
230
|
Returns:
|
|
231
|
+
-------
|
|
200
232
|
Linear issue input dictionary
|
|
233
|
+
|
|
201
234
|
"""
|
|
202
235
|
from .types import get_linear_priority
|
|
203
|
-
|
|
204
|
-
issue_input = {
|
|
236
|
+
|
|
237
|
+
issue_input: dict[str, Any] = {
|
|
205
238
|
"title": task.title,
|
|
206
239
|
"teamId": team_id,
|
|
207
240
|
}
|
|
208
|
-
|
|
241
|
+
|
|
209
242
|
# Add description if provided
|
|
210
243
|
if task.description:
|
|
211
244
|
issue_input["description"] = task.description
|
|
212
|
-
|
|
245
|
+
|
|
213
246
|
# Add priority
|
|
214
247
|
if task.priority:
|
|
215
248
|
issue_input["priority"] = get_linear_priority(task.priority)
|
|
216
|
-
|
|
249
|
+
|
|
217
250
|
# Add assignee if provided (assumes it's a user ID)
|
|
218
251
|
if task.assignee:
|
|
219
252
|
issue_input["assigneeId"] = task.assignee
|
|
220
|
-
|
|
253
|
+
|
|
221
254
|
# Add parent issue if provided
|
|
222
255
|
if task.parent_issue:
|
|
223
256
|
issue_input["parentId"] = task.parent_issue
|
|
224
|
-
|
|
257
|
+
|
|
225
258
|
# Add project (epic) if provided
|
|
226
259
|
if task.parent_epic:
|
|
227
260
|
issue_input["projectId"] = task.parent_epic
|
|
228
|
-
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
|
|
262
|
+
# DO NOT set labelIds here - adapter will handle label resolution
|
|
263
|
+
# Labels are resolved from names to UUIDs in LinearAdapter._create_task()
|
|
264
|
+
# If we set it here, we create a type mismatch (names vs UUIDs)
|
|
265
|
+
# The adapter's label resolution logic (lines 982-988) handles this properly
|
|
266
|
+
#
|
|
267
|
+
# Bug Fix (v1.1.1): Previously, setting labelIds to tag names here caused
|
|
268
|
+
# "Argument Validation Error" from Linear's GraphQL API. The API requires
|
|
269
|
+
# UUIDs (e.g., "uuid-1"), not names (e.g., "bug"). This was fixed by:
|
|
270
|
+
# 1. Removing labelIds assignment in mapper (this file)
|
|
271
|
+
# 2. Adding UUID validation in adapter._create_task() (adapter.py:1047-1060)
|
|
272
|
+
# 3. Changing GraphQL labelIds type to [String!]! (queries.py)
|
|
273
|
+
#
|
|
274
|
+
# See: docs/TROUBLESHOOTING.md#issue-argument-validation-error-when-creating-issues-with-labels
|
|
275
|
+
|
|
235
276
|
# Add Linear-specific metadata
|
|
236
277
|
if task.metadata and "linear" in task.metadata:
|
|
237
278
|
linear_meta = task.metadata["linear"]
|
|
@@ -241,42 +282,49 @@ def build_linear_issue_input(task: Task, team_id: str) -> Dict[str, Any]:
|
|
|
241
282
|
issue_input["cycleId"] = linear_meta["cycle_id"]
|
|
242
283
|
if "estimate" in linear_meta:
|
|
243
284
|
issue_input["estimate"] = linear_meta["estimate"]
|
|
244
|
-
|
|
285
|
+
|
|
245
286
|
return issue_input
|
|
246
287
|
|
|
247
288
|
|
|
248
|
-
def build_linear_issue_update_input(updates:
|
|
289
|
+
def build_linear_issue_update_input(updates: dict[str, Any]) -> dict[str, Any]:
|
|
249
290
|
"""Build Linear issue update input from update dictionary.
|
|
250
|
-
|
|
291
|
+
|
|
251
292
|
Args:
|
|
293
|
+
----
|
|
252
294
|
updates: Dictionary of fields to update
|
|
253
|
-
|
|
295
|
+
|
|
254
296
|
Returns:
|
|
297
|
+
-------
|
|
255
298
|
Linear issue update input dictionary
|
|
299
|
+
|
|
256
300
|
"""
|
|
257
301
|
from .types import get_linear_priority
|
|
258
|
-
|
|
302
|
+
|
|
259
303
|
update_input = {}
|
|
260
|
-
|
|
304
|
+
|
|
261
305
|
# Map standard fields
|
|
262
306
|
if "title" in updates:
|
|
263
307
|
update_input["title"] = updates["title"]
|
|
264
|
-
|
|
308
|
+
|
|
265
309
|
if "description" in updates:
|
|
266
310
|
update_input["description"] = updates["description"]
|
|
267
|
-
|
|
311
|
+
|
|
268
312
|
if "priority" in updates:
|
|
269
|
-
priority =
|
|
313
|
+
priority = (
|
|
314
|
+
Priority(updates["priority"])
|
|
315
|
+
if isinstance(updates["priority"], str)
|
|
316
|
+
else updates["priority"]
|
|
317
|
+
)
|
|
270
318
|
update_input["priority"] = get_linear_priority(priority)
|
|
271
|
-
|
|
319
|
+
|
|
272
320
|
if "assignee" in updates:
|
|
273
321
|
update_input["assigneeId"] = updates["assignee"]
|
|
274
|
-
|
|
322
|
+
|
|
275
323
|
# Handle state transitions (would need workflow state mapping)
|
|
276
324
|
if "state" in updates:
|
|
277
325
|
# This would need to be handled by the adapter with proper state mapping
|
|
278
326
|
pass
|
|
279
|
-
|
|
327
|
+
|
|
280
328
|
# Handle metadata updates
|
|
281
329
|
if "metadata" in updates and "linear" in updates["metadata"]:
|
|
282
330
|
linear_meta = updates["metadata"]["linear"]
|
|
@@ -288,20 +336,192 @@ def build_linear_issue_update_input(updates: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
288
336
|
update_input["projectId"] = linear_meta["project_id"]
|
|
289
337
|
if "estimate" in linear_meta:
|
|
290
338
|
update_input["estimate"] = linear_meta["estimate"]
|
|
291
|
-
|
|
339
|
+
|
|
292
340
|
return update_input
|
|
293
341
|
|
|
294
342
|
|
|
295
|
-
def extract_child_issue_ids(issue_data:
|
|
343
|
+
def extract_child_issue_ids(issue_data: dict[str, Any]) -> list[str]:
|
|
296
344
|
"""Extract child issue IDs from Linear issue data.
|
|
297
|
-
|
|
345
|
+
|
|
298
346
|
Args:
|
|
347
|
+
----
|
|
299
348
|
issue_data: Raw Linear issue data from GraphQL
|
|
300
|
-
|
|
349
|
+
|
|
301
350
|
Returns:
|
|
351
|
+
-------
|
|
302
352
|
List of child issue identifiers
|
|
353
|
+
|
|
303
354
|
"""
|
|
304
355
|
child_ids = []
|
|
305
356
|
if issue_data.get("children", {}).get("nodes"):
|
|
306
357
|
child_ids = [child["identifier"] for child in issue_data["children"]["nodes"]]
|
|
307
358
|
return child_ids
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def map_linear_attachment_to_attachment(
|
|
362
|
+
attachment_data: dict[str, Any], ticket_id: str
|
|
363
|
+
) -> Attachment:
|
|
364
|
+
"""Convert Linear attachment data to universal Attachment model.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
----
|
|
368
|
+
attachment_data: Raw Linear attachment data from GraphQL
|
|
369
|
+
ticket_id: ID of the ticket this attachment belongs to
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
-------
|
|
373
|
+
Universal Attachment model
|
|
374
|
+
|
|
375
|
+
Note:
|
|
376
|
+
----
|
|
377
|
+
Linear attachment URLs require authentication with API key.
|
|
378
|
+
URLs are in format: https://files.linear.app/workspace/attachment-id/filename
|
|
379
|
+
Authentication header: Authorization: Bearer {api_key}
|
|
380
|
+
|
|
381
|
+
"""
|
|
382
|
+
# Extract basic fields
|
|
383
|
+
attachment_id = attachment_data.get("id")
|
|
384
|
+
title = attachment_data.get("title", "Untitled")
|
|
385
|
+
url = attachment_data.get("url")
|
|
386
|
+
subtitle = attachment_data.get("subtitle")
|
|
387
|
+
|
|
388
|
+
# Extract dates
|
|
389
|
+
created_at = None
|
|
390
|
+
if attachment_data.get("createdAt"):
|
|
391
|
+
created_at = datetime.fromisoformat(
|
|
392
|
+
attachment_data["createdAt"].replace("Z", "+00:00")
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if attachment_data.get("updatedAt"):
|
|
396
|
+
datetime.fromisoformat(attachment_data["updatedAt"].replace("Z", "+00:00"))
|
|
397
|
+
|
|
398
|
+
# Build metadata with Linear-specific fields
|
|
399
|
+
metadata = {
|
|
400
|
+
"linear": {
|
|
401
|
+
"id": attachment_id,
|
|
402
|
+
"title": title,
|
|
403
|
+
"subtitle": subtitle,
|
|
404
|
+
"metadata": attachment_data.get("metadata"),
|
|
405
|
+
"updated_at": attachment_data.get("updatedAt"),
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return Attachment(
|
|
410
|
+
id=attachment_id,
|
|
411
|
+
ticket_id=ticket_id,
|
|
412
|
+
filename=title, # Linear uses 'title' as filename
|
|
413
|
+
url=url,
|
|
414
|
+
content_type=None, # Linear doesn't provide MIME type in GraphQL
|
|
415
|
+
size_bytes=None, # Linear doesn't provide size in GraphQL
|
|
416
|
+
created_at=created_at,
|
|
417
|
+
created_by=None, # Not included in standard fragment
|
|
418
|
+
description=subtitle,
|
|
419
|
+
metadata=metadata,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def task_to_compact_format(task: Task) -> dict[str, Any]:
|
|
424
|
+
"""Convert Task to compact format for efficient token usage.
|
|
425
|
+
|
|
426
|
+
Compact format includes only essential fields, reducing token usage by ~70-80%
|
|
427
|
+
compared to full Task serialization.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
----
|
|
431
|
+
task: Universal Task model
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
-------
|
|
435
|
+
Compact dictionary with minimal essential fields
|
|
436
|
+
|
|
437
|
+
Design Decision: Compact Format Fields (1M-554)
|
|
438
|
+
-----------------------------------------------
|
|
439
|
+
Rationale: Selected fields based on token efficiency analysis and user needs.
|
|
440
|
+
Full Task serialization averages ~600 tokens per item. Compact format targets
|
|
441
|
+
~120 tokens per item (80% reduction).
|
|
442
|
+
|
|
443
|
+
Essential fields included:
|
|
444
|
+
- id: Required for all operations (identifier)
|
|
445
|
+
- title: Primary user-facing information
|
|
446
|
+
- state: Critical for workflow understanding
|
|
447
|
+
- priority: Important for triage and filtering
|
|
448
|
+
- assignee: Key for task assignment visibility
|
|
449
|
+
|
|
450
|
+
Fields excluded:
|
|
451
|
+
- description: Often large (100-500 tokens), available via get()
|
|
452
|
+
- creator: Less critical for list views
|
|
453
|
+
- tags: Available in full format, not essential for scanning
|
|
454
|
+
- children: Hierarchy details available via dedicated queries
|
|
455
|
+
- created_at/updated_at: Not essential for list scanning
|
|
456
|
+
- metadata: Platform-specific, not needed in compact view
|
|
457
|
+
|
|
458
|
+
Performance: Reduces typical 50-item list from ~30,000 to ~6,000 tokens.
|
|
459
|
+
|
|
460
|
+
"""
|
|
461
|
+
# Handle state - can be TicketState enum or string
|
|
462
|
+
state_value = None
|
|
463
|
+
if task.state:
|
|
464
|
+
state_value = task.state.value if hasattr(task.state, "value") else task.state
|
|
465
|
+
|
|
466
|
+
# Handle priority - can be Priority enum or string
|
|
467
|
+
priority_value = None
|
|
468
|
+
if task.priority:
|
|
469
|
+
priority_value = (
|
|
470
|
+
task.priority.value if hasattr(task.priority, "value") else task.priority
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
"id": task.id,
|
|
475
|
+
"title": task.title,
|
|
476
|
+
"state": state_value,
|
|
477
|
+
"priority": priority_value,
|
|
478
|
+
"assignee": task.assignee,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def epic_to_compact_format(epic: Epic) -> dict[str, Any]:
|
|
483
|
+
"""Convert Epic to compact format for efficient token usage.
|
|
484
|
+
|
|
485
|
+
Compact format includes only essential fields, reducing token usage by ~70-80%
|
|
486
|
+
compared to full Epic serialization.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
----
|
|
490
|
+
epic: Universal Epic model
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
-------
|
|
494
|
+
Compact dictionary with minimal essential fields
|
|
495
|
+
|
|
496
|
+
Design Decision: Epic Compact Format (1M-554)
|
|
497
|
+
---------------------------------------------
|
|
498
|
+
Rationale: Epics typically have less metadata than tasks, but descriptions
|
|
499
|
+
can still be large. Compact format focuses on overview information.
|
|
500
|
+
|
|
501
|
+
Essential fields:
|
|
502
|
+
- id: Required for all operations
|
|
503
|
+
- title: Primary identifier
|
|
504
|
+
- state: Project status
|
|
505
|
+
- child_count: Useful for project overview (if available)
|
|
506
|
+
|
|
507
|
+
Performance: Similar token reduction to task compact format.
|
|
508
|
+
|
|
509
|
+
"""
|
|
510
|
+
# Handle state - can be TicketState enum or string
|
|
511
|
+
state_value = None
|
|
512
|
+
if epic.state:
|
|
513
|
+
state_value = epic.state.value if hasattr(epic.state, "value") else epic.state
|
|
514
|
+
|
|
515
|
+
compact = {
|
|
516
|
+
"id": epic.id,
|
|
517
|
+
"title": epic.title,
|
|
518
|
+
"state": state_value,
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# Include child count if available in metadata
|
|
522
|
+
if epic.metadata and "linear" in epic.metadata:
|
|
523
|
+
linear_meta = epic.metadata["linear"]
|
|
524
|
+
if "issue_count" in linear_meta:
|
|
525
|
+
compact["child_count"] = linear_meta["issue_count"]
|
|
526
|
+
|
|
527
|
+
return compact
|