mcp-ticketer 0.12.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +385 -6
- mcp_ticketer/adapters/asana/adapter.py +108 -0
- mcp_ticketer/adapters/asana/mappers.py +14 -0
- mcp_ticketer/adapters/github.py +525 -11
- mcp_ticketer/adapters/hybrid.py +47 -5
- mcp_ticketer/adapters/jira.py +521 -0
- mcp_ticketer/adapters/linear/adapter.py +1784 -101
- mcp_ticketer/adapters/linear/client.py +85 -3
- mcp_ticketer/adapters/linear/mappers.py +96 -8
- mcp_ticketer/adapters/linear/queries.py +168 -1
- mcp_ticketer/adapters/linear/types.py +80 -4
- mcp_ticketer/analysis/__init__.py +56 -0
- mcp_ticketer/analysis/dependency_graph.py +255 -0
- mcp_ticketer/analysis/health_assessment.py +304 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/project_status.py +594 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/automation/__init__.py +11 -0
- mcp_ticketer/automation/project_updates.py +378 -0
- mcp_ticketer/cli/adapter_diagnostics.py +3 -1
- mcp_ticketer/cli/auggie_configure.py +17 -5
- mcp_ticketer/cli/codex_configure.py +97 -61
- mcp_ticketer/cli/configure.py +851 -103
- mcp_ticketer/cli/cursor_configure.py +314 -0
- mcp_ticketer/cli/diagnostics.py +13 -12
- mcp_ticketer/cli/discover.py +5 -0
- mcp_ticketer/cli/gemini_configure.py +17 -5
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +6 -0
- mcp_ticketer/cli/main.py +233 -3151
- mcp_ticketer/cli/mcp_configure.py +672 -98
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/platform_detection.py +77 -12
- mcp_ticketer/cli/platform_installer.py +536 -0
- mcp_ticketer/cli/project_update_commands.py +350 -0
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +12 -10
- mcp_ticketer/cli/ticket_commands.py +264 -24
- mcp_ticketer/core/__init__.py +28 -6
- mcp_ticketer/core/adapter.py +166 -1
- mcp_ticketer/core/config.py +21 -21
- mcp_ticketer/core/exceptions.py +7 -1
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +31 -19
- mcp_ticketer/core/models.py +135 -0
- mcp_ticketer/core/onepassword_secrets.py +1 -1
- mcp_ticketer/core/priority_matcher.py +463 -0
- mcp_ticketer/core/project_config.py +132 -14
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/main.py +106 -25
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +58 -0
- mcp_ticketer/mcp/server/tools/__init__.py +31 -12
- mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +6 -8
- mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
- mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
- mcp_ticketer/mcp/server/tools/config_tools.py +1184 -136
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +870 -460
- mcp_ticketer/mcp/server/tools/instruction_tools.py +7 -5
- mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
- mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
- mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
- mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1070 -123
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +218 -236
- mcp_ticketer/queue/worker.py +1 -1
- mcp_ticketer/utils/__init__.py +5 -0
- mcp_ticketer/utils/token_utils.py +246 -0
- mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
- mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
- mcp_ticketer-0.12.0.dist-info/METADATA +0 -550
- mcp_ticketer-0.12.0.dist-info/RECORD +0 -91
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.12.0.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ from typing import Any
|
|
|
7
7
|
|
|
8
8
|
try:
|
|
9
9
|
from gql import Client, gql
|
|
10
|
-
from gql.transport.exceptions import TransportError
|
|
10
|
+
from gql.transport.exceptions import TransportError, TransportQueryError
|
|
11
11
|
from gql.transport.httpx import HTTPXAsyncTransport
|
|
12
12
|
except ImportError:
|
|
13
13
|
# Handle missing gql dependency gracefully
|
|
@@ -15,6 +15,7 @@ except ImportError:
|
|
|
15
15
|
gql = None
|
|
16
16
|
HTTPXAsyncTransport = None
|
|
17
17
|
TransportError = Exception
|
|
18
|
+
TransportQueryError = Exception
|
|
18
19
|
|
|
19
20
|
from ...core.exceptions import AdapterError, AuthenticationError, RateLimitError
|
|
20
21
|
|
|
@@ -26,6 +27,7 @@ class LinearGraphQLClient:
|
|
|
26
27
|
"""Initialize the Linear GraphQL client.
|
|
27
28
|
|
|
28
29
|
Args:
|
|
30
|
+
----
|
|
29
31
|
api_key: Linear API key
|
|
30
32
|
timeout: Request timeout in seconds
|
|
31
33
|
|
|
@@ -38,9 +40,11 @@ class LinearGraphQLClient:
|
|
|
38
40
|
"""Create a new GraphQL client instance.
|
|
39
41
|
|
|
40
42
|
Returns:
|
|
43
|
+
-------
|
|
41
44
|
Configured GraphQL client
|
|
42
45
|
|
|
43
46
|
Raises:
|
|
47
|
+
------
|
|
44
48
|
AuthenticationError: If API key is invalid
|
|
45
49
|
AdapterError: If client creation fails
|
|
46
50
|
|
|
@@ -80,14 +84,17 @@ class LinearGraphQLClient:
|
|
|
80
84
|
"""Execute a GraphQL query with error handling and retries.
|
|
81
85
|
|
|
82
86
|
Args:
|
|
87
|
+
----
|
|
83
88
|
query_string: GraphQL query string
|
|
84
89
|
variables: Query variables
|
|
85
90
|
retries: Number of retry attempts
|
|
86
91
|
|
|
87
92
|
Returns:
|
|
93
|
+
-------
|
|
88
94
|
Query result data
|
|
89
95
|
|
|
90
96
|
Raises:
|
|
97
|
+
------
|
|
91
98
|
AuthenticationError: If authentication fails
|
|
92
99
|
RateLimitError: If rate limit is exceeded
|
|
93
100
|
AdapterError: If query execution fails
|
|
@@ -104,6 +111,33 @@ class LinearGraphQLClient:
|
|
|
104
111
|
)
|
|
105
112
|
return result
|
|
106
113
|
|
|
114
|
+
except TransportQueryError as e:
|
|
115
|
+
"""
|
|
116
|
+
Handle GraphQL validation errors (e.g., duplicate label names).
|
|
117
|
+
TransportQueryError is a subclass of TransportError with .errors attribute.
|
|
118
|
+
|
|
119
|
+
Related: 1M-398 - Label duplicate error handling
|
|
120
|
+
"""
|
|
121
|
+
if e.errors:
|
|
122
|
+
error_msg = e.errors[0].get("message", "Unknown GraphQL error")
|
|
123
|
+
|
|
124
|
+
# Check for duplicate label errors specifically
|
|
125
|
+
if (
|
|
126
|
+
"duplicate" in error_msg.lower()
|
|
127
|
+
and "label" in error_msg.lower()
|
|
128
|
+
):
|
|
129
|
+
raise AdapterError(
|
|
130
|
+
f"Label already exists: {error_msg}", "linear"
|
|
131
|
+
) from e
|
|
132
|
+
|
|
133
|
+
# Other validation errors
|
|
134
|
+
raise AdapterError(
|
|
135
|
+
f"Linear GraphQL validation error: {error_msg}", "linear"
|
|
136
|
+
) from e
|
|
137
|
+
|
|
138
|
+
# Fallback if no errors attribute
|
|
139
|
+
raise AdapterError(f"Linear GraphQL error: {e}", "linear") from e
|
|
140
|
+
|
|
107
141
|
except TransportError as e:
|
|
108
142
|
# Handle HTTP errors
|
|
109
143
|
if hasattr(e, "response") and e.response:
|
|
@@ -175,14 +209,17 @@ class LinearGraphQLClient:
|
|
|
175
209
|
"""Execute a GraphQL mutation with error handling.
|
|
176
210
|
|
|
177
211
|
Args:
|
|
212
|
+
----
|
|
178
213
|
mutation_string: GraphQL mutation string
|
|
179
214
|
variables: Mutation variables
|
|
180
215
|
retries: Number of retry attempts
|
|
181
216
|
|
|
182
217
|
Returns:
|
|
218
|
+
-------
|
|
183
219
|
Mutation result data
|
|
184
220
|
|
|
185
221
|
Raises:
|
|
222
|
+
------
|
|
186
223
|
AuthenticationError: If authentication fails
|
|
187
224
|
RateLimitError: If rate limit is exceeded
|
|
188
225
|
AdapterError: If mutation execution fails
|
|
@@ -194,9 +231,20 @@ class LinearGraphQLClient:
|
|
|
194
231
|
"""Test the connection to Linear API.
|
|
195
232
|
|
|
196
233
|
Returns:
|
|
234
|
+
-------
|
|
197
235
|
True if connection is successful, False otherwise
|
|
198
236
|
|
|
237
|
+
Design Decision: Enhanced Debug Logging (1M-431)
|
|
238
|
+
-------------------------------------------------
|
|
239
|
+
Added comprehensive logging to diagnose connection failures.
|
|
240
|
+
Logs API key preview, query results, and specific failure reasons
|
|
241
|
+
to help users troubleshoot authentication and configuration issues.
|
|
242
|
+
|
|
199
243
|
"""
|
|
244
|
+
import logging
|
|
245
|
+
|
|
246
|
+
logger = logging.getLogger(__name__)
|
|
247
|
+
|
|
200
248
|
try:
|
|
201
249
|
# Simple query to test authentication
|
|
202
250
|
test_query = """
|
|
@@ -204,23 +252,53 @@ class LinearGraphQLClient:
|
|
|
204
252
|
viewer {
|
|
205
253
|
id
|
|
206
254
|
name
|
|
255
|
+
email
|
|
207
256
|
}
|
|
208
257
|
}
|
|
209
258
|
"""
|
|
210
259
|
|
|
260
|
+
logger.debug(
|
|
261
|
+
f"Testing Linear API connection with API key: {self.api_key[:20]}..."
|
|
262
|
+
)
|
|
211
263
|
result = await self.execute_query(test_query)
|
|
212
|
-
return bool(result.get("viewer"))
|
|
213
264
|
|
|
214
|
-
|
|
265
|
+
# Log the actual response for debugging
|
|
266
|
+
logger.debug(f"Linear API test response: {result}")
|
|
267
|
+
|
|
268
|
+
viewer = result.get("viewer")
|
|
269
|
+
|
|
270
|
+
if not viewer:
|
|
271
|
+
logger.warning(
|
|
272
|
+
f"Linear test connection query succeeded but returned no viewer data. "
|
|
273
|
+
f"Response: {result}"
|
|
274
|
+
)
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
if not viewer.get("id"):
|
|
278
|
+
logger.warning(f"Linear viewer missing id field. Viewer data: {viewer}")
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
logger.info(
|
|
282
|
+
f"Linear API connected successfully as: {viewer.get('name')} ({viewer.get('email')})"
|
|
283
|
+
)
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(
|
|
288
|
+
f"Linear connection test failed: {type(e).__name__}: {e}",
|
|
289
|
+
exc_info=True,
|
|
290
|
+
)
|
|
215
291
|
return False
|
|
216
292
|
|
|
217
293
|
async def get_team_info(self, team_id: str) -> dict[str, Any] | None:
|
|
218
294
|
"""Get team information by ID.
|
|
219
295
|
|
|
220
296
|
Args:
|
|
297
|
+
----
|
|
221
298
|
team_id: Linear team ID
|
|
222
299
|
|
|
223
300
|
Returns:
|
|
301
|
+
-------
|
|
224
302
|
Team information or None if not found
|
|
225
303
|
|
|
226
304
|
"""
|
|
@@ -246,9 +324,11 @@ class LinearGraphQLClient:
|
|
|
246
324
|
"""Get user information by email.
|
|
247
325
|
|
|
248
326
|
Args:
|
|
327
|
+
----
|
|
249
328
|
email: User email address
|
|
250
329
|
|
|
251
330
|
Returns:
|
|
331
|
+
-------
|
|
252
332
|
User information or None if not found
|
|
253
333
|
|
|
254
334
|
"""
|
|
@@ -278,9 +358,11 @@ class LinearGraphQLClient:
|
|
|
278
358
|
"""Search users by display name or full name.
|
|
279
359
|
|
|
280
360
|
Args:
|
|
361
|
+
----
|
|
281
362
|
name: Display name or full name to search for
|
|
282
363
|
|
|
283
364
|
Returns:
|
|
365
|
+
-------
|
|
284
366
|
List of matching users (may be empty)
|
|
285
367
|
|
|
286
368
|
"""
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from ...core.models import Comment, Epic, Priority, Task, TicketState
|
|
8
|
+
from ...core.models import Attachment, Comment, Epic, Priority, Task, TicketState
|
|
9
9
|
from .types import extract_linear_metadata, get_universal_priority, get_universal_state
|
|
10
10
|
|
|
11
11
|
|
|
@@ -16,9 +16,11 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
|
|
|
16
16
|
with a parent issue).
|
|
17
17
|
|
|
18
18
|
Args:
|
|
19
|
+
----
|
|
19
20
|
issue_data: Raw Linear issue data from GraphQL
|
|
20
21
|
|
|
21
22
|
Returns:
|
|
23
|
+
-------
|
|
22
24
|
Universal Task model
|
|
23
25
|
|
|
24
26
|
"""
|
|
@@ -31,10 +33,11 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
|
|
|
31
33
|
linear_priority = issue_data.get("priority", 3)
|
|
32
34
|
priority = get_universal_priority(linear_priority)
|
|
33
35
|
|
|
34
|
-
# Map state
|
|
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
|
-
|
|
39
|
+
state_name = state_data.get("name") # Extract state name for synonym matching
|
|
40
|
+
state = get_universal_state(state_type, state_name)
|
|
38
41
|
|
|
39
42
|
# Extract assignee
|
|
40
43
|
assignee = None
|
|
@@ -76,6 +79,9 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
|
|
|
76
79
|
issue_data["updatedAt"].replace("Z", "+00:00")
|
|
77
80
|
)
|
|
78
81
|
|
|
82
|
+
# Extract child issue IDs
|
|
83
|
+
children = extract_child_issue_ids(issue_data)
|
|
84
|
+
|
|
79
85
|
# Extract Linear-specific metadata
|
|
80
86
|
linear_metadata = extract_linear_metadata(issue_data)
|
|
81
87
|
metadata = {"linear": linear_metadata} if linear_metadata else {}
|
|
@@ -91,6 +97,7 @@ def map_linear_issue_to_task(issue_data: dict[str, Any]) -> Task:
|
|
|
91
97
|
tags=tags,
|
|
92
98
|
parent_epic=parent_epic,
|
|
93
99
|
parent_issue=parent_issue,
|
|
100
|
+
children=children,
|
|
94
101
|
created_at=created_at,
|
|
95
102
|
updated_at=updated_at,
|
|
96
103
|
metadata=metadata,
|
|
@@ -101,9 +108,11 @@ def map_linear_project_to_epic(project_data: dict[str, Any]) -> Epic:
|
|
|
101
108
|
"""Convert Linear project data to universal Epic model.
|
|
102
109
|
|
|
103
110
|
Args:
|
|
111
|
+
----
|
|
104
112
|
project_data: Raw Linear project data from GraphQL
|
|
105
113
|
|
|
106
114
|
Returns:
|
|
115
|
+
-------
|
|
107
116
|
Universal Epic model
|
|
108
117
|
|
|
109
118
|
"""
|
|
@@ -137,7 +146,7 @@ def map_linear_project_to_epic(project_data: dict[str, Any]) -> Epic:
|
|
|
137
146
|
)
|
|
138
147
|
|
|
139
148
|
# Extract Linear-specific metadata
|
|
140
|
-
metadata = {"linear": {}}
|
|
149
|
+
metadata: dict[str, Any] = {"linear": {}}
|
|
141
150
|
if project_data.get("url"):
|
|
142
151
|
metadata["linear"]["linear_url"] = project_data["url"]
|
|
143
152
|
if project_data.get("icon"):
|
|
@@ -165,10 +174,12 @@ def map_linear_comment_to_comment(
|
|
|
165
174
|
"""Convert Linear comment data to universal Comment model.
|
|
166
175
|
|
|
167
176
|
Args:
|
|
177
|
+
----
|
|
168
178
|
comment_data: Raw Linear comment data from GraphQL
|
|
169
179
|
ticket_id: ID of the ticket this comment belongs to
|
|
170
180
|
|
|
171
181
|
Returns:
|
|
182
|
+
-------
|
|
172
183
|
Universal Comment model
|
|
173
184
|
|
|
174
185
|
"""
|
|
@@ -212,10 +223,12 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
|
|
|
212
223
|
or for a sub-issue when task.parent_issue is provided.
|
|
213
224
|
|
|
214
225
|
Args:
|
|
226
|
+
----
|
|
215
227
|
task: Universal Task model
|
|
216
228
|
team_id: Linear team ID
|
|
217
229
|
|
|
218
230
|
Returns:
|
|
231
|
+
-------
|
|
219
232
|
Linear issue input dictionary
|
|
220
233
|
|
|
221
234
|
"""
|
|
@@ -246,10 +259,19 @@ def build_linear_issue_input(task: Task, team_id: str) -> dict[str, Any]:
|
|
|
246
259
|
if task.parent_epic:
|
|
247
260
|
issue_input["projectId"] = task.parent_epic
|
|
248
261
|
|
|
249
|
-
#
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
253
275
|
|
|
254
276
|
# Add Linear-specific metadata
|
|
255
277
|
if task.metadata and "linear" in task.metadata:
|
|
@@ -268,9 +290,11 @@ def build_linear_issue_update_input(updates: dict[str, Any]) -> dict[str, Any]:
|
|
|
268
290
|
"""Build Linear issue update input from update dictionary.
|
|
269
291
|
|
|
270
292
|
Args:
|
|
293
|
+
----
|
|
271
294
|
updates: Dictionary of fields to update
|
|
272
295
|
|
|
273
296
|
Returns:
|
|
297
|
+
-------
|
|
274
298
|
Linear issue update input dictionary
|
|
275
299
|
|
|
276
300
|
"""
|
|
@@ -320,9 +344,11 @@ def extract_child_issue_ids(issue_data: dict[str, Any]) -> list[str]:
|
|
|
320
344
|
"""Extract child issue IDs from Linear issue data.
|
|
321
345
|
|
|
322
346
|
Args:
|
|
347
|
+
----
|
|
323
348
|
issue_data: Raw Linear issue data from GraphQL
|
|
324
349
|
|
|
325
350
|
Returns:
|
|
351
|
+
-------
|
|
326
352
|
List of child issue identifiers
|
|
327
353
|
|
|
328
354
|
"""
|
|
@@ -330,3 +356,65 @@ def extract_child_issue_ids(issue_data: dict[str, Any]) -> list[str]:
|
|
|
330
356
|
if issue_data.get("children", {}).get("nodes"):
|
|
331
357
|
child_ids = [child["identifier"] for child in issue_data["children"]["nodes"]]
|
|
332
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
|
+
)
|
|
@@ -351,7 +351,8 @@ SEARCH_ISSUE_BY_IDENTIFIER_QUERY = """
|
|
|
351
351
|
"""
|
|
352
352
|
|
|
353
353
|
LIST_PROJECTS_QUERY = (
|
|
354
|
-
PROJECT_FRAGMENT
|
|
354
|
+
TEAM_FRAGMENT # Required by PROJECT_FRAGMENT which uses ...TeamFields
|
|
355
|
+
+ PROJECT_FRAGMENT
|
|
355
356
|
+ """
|
|
356
357
|
query ListProjects($filter: ProjectFilter, $first: Int!) {
|
|
357
358
|
projects(filter: $filter, first: $first, orderBy: updatedAt) {
|
|
@@ -387,3 +388,169 @@ GET_CURRENT_USER_QUERY = (
|
|
|
387
388
|
}
|
|
388
389
|
"""
|
|
389
390
|
)
|
|
391
|
+
|
|
392
|
+
CREATE_LABEL_MUTATION = """
|
|
393
|
+
mutation CreateLabel($input: IssueLabelCreateInput!) {
|
|
394
|
+
issueLabelCreate(input: $input) {
|
|
395
|
+
success
|
|
396
|
+
issueLabel {
|
|
397
|
+
id
|
|
398
|
+
name
|
|
399
|
+
color
|
|
400
|
+
description
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
LIST_CYCLES_QUERY = """
|
|
407
|
+
query GetCycles($teamId: String!, $first: Int!, $after: String) {
|
|
408
|
+
team(id: $teamId) {
|
|
409
|
+
cycles(first: $first, after: $after) {
|
|
410
|
+
nodes {
|
|
411
|
+
id
|
|
412
|
+
name
|
|
413
|
+
number
|
|
414
|
+
startsAt
|
|
415
|
+
endsAt
|
|
416
|
+
completedAt
|
|
417
|
+
progress
|
|
418
|
+
}
|
|
419
|
+
pageInfo {
|
|
420
|
+
hasNextPage
|
|
421
|
+
endCursor
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
GET_ISSUE_STATUS_QUERY = """
|
|
429
|
+
query GetIssueStatus($issueId: String!) {
|
|
430
|
+
issue(id: $issueId) {
|
|
431
|
+
id
|
|
432
|
+
state {
|
|
433
|
+
id
|
|
434
|
+
name
|
|
435
|
+
type
|
|
436
|
+
color
|
|
437
|
+
description
|
|
438
|
+
position
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
"""
|
|
443
|
+
|
|
444
|
+
LIST_ISSUE_STATUSES_QUERY = """
|
|
445
|
+
query GetWorkflowStates($teamId: String!) {
|
|
446
|
+
team(id: $teamId) {
|
|
447
|
+
states {
|
|
448
|
+
nodes {
|
|
449
|
+
id
|
|
450
|
+
name
|
|
451
|
+
type
|
|
452
|
+
color
|
|
453
|
+
description
|
|
454
|
+
position
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
GET_CUSTOM_VIEW_QUERY = (
|
|
462
|
+
ISSUE_LIST_FRAGMENTS
|
|
463
|
+
+ """
|
|
464
|
+
query GetCustomView($viewId: String!, $first: Int!) {
|
|
465
|
+
customView(id: $viewId) {
|
|
466
|
+
id
|
|
467
|
+
name
|
|
468
|
+
description
|
|
469
|
+
issues(first: $first) {
|
|
470
|
+
nodes {
|
|
471
|
+
...IssueCompactFields
|
|
472
|
+
}
|
|
473
|
+
pageInfo {
|
|
474
|
+
hasNextPage
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
"""
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Project Update queries and mutations (1M-238)
|
|
483
|
+
|
|
484
|
+
PROJECT_UPDATE_FRAGMENT = """
|
|
485
|
+
fragment ProjectUpdateFields on ProjectUpdate {
|
|
486
|
+
id
|
|
487
|
+
body
|
|
488
|
+
health
|
|
489
|
+
createdAt
|
|
490
|
+
updatedAt
|
|
491
|
+
diffMarkdown
|
|
492
|
+
url
|
|
493
|
+
user {
|
|
494
|
+
id
|
|
495
|
+
name
|
|
496
|
+
email
|
|
497
|
+
}
|
|
498
|
+
project {
|
|
499
|
+
id
|
|
500
|
+
name
|
|
501
|
+
slugId
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
CREATE_PROJECT_UPDATE_MUTATION = (
|
|
507
|
+
PROJECT_UPDATE_FRAGMENT
|
|
508
|
+
+ """
|
|
509
|
+
mutation ProjectUpdateCreate(
|
|
510
|
+
$projectId: String!
|
|
511
|
+
$body: String!
|
|
512
|
+
$health: ProjectUpdateHealthType
|
|
513
|
+
) {
|
|
514
|
+
projectUpdateCreate(
|
|
515
|
+
input: {
|
|
516
|
+
projectId: $projectId
|
|
517
|
+
body: $body
|
|
518
|
+
health: $health
|
|
519
|
+
}
|
|
520
|
+
) {
|
|
521
|
+
success
|
|
522
|
+
projectUpdate {
|
|
523
|
+
...ProjectUpdateFields
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
"""
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
LIST_PROJECT_UPDATES_QUERY = (
|
|
531
|
+
PROJECT_UPDATE_FRAGMENT
|
|
532
|
+
+ """
|
|
533
|
+
query ProjectUpdates($projectId: String!, $first: Int) {
|
|
534
|
+
project(id: $projectId) {
|
|
535
|
+
id
|
|
536
|
+
name
|
|
537
|
+
projectUpdates(first: $first) {
|
|
538
|
+
nodes {
|
|
539
|
+
...ProjectUpdateFields
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
"""
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
GET_PROJECT_UPDATE_QUERY = (
|
|
548
|
+
PROJECT_UPDATE_FRAGMENT
|
|
549
|
+
+ """
|
|
550
|
+
query ProjectUpdate($id: String!) {
|
|
551
|
+
projectUpdate(id: $id) {
|
|
552
|
+
...ProjectUpdateFields
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
"""
|
|
556
|
+
)
|