mcp-ticketer 0.3.1__py3-none-any.whl → 0.3.3__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/__version__.py +1 -1
- mcp_ticketer/adapters/aitrackdown.py +164 -36
- mcp_ticketer/adapters/github.py +11 -8
- mcp_ticketer/adapters/jira.py +29 -28
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +105 -104
- mcp_ticketer/adapters/linear/client.py +78 -59
- mcp_ticketer/adapters/linear/mappers.py +93 -73
- mcp_ticketer/adapters/linear/queries.py +28 -7
- mcp_ticketer/adapters/linear/types.py +67 -60
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/cli/adapter_diagnostics.py +87 -52
- mcp_ticketer/cli/codex_configure.py +6 -6
- mcp_ticketer/cli/diagnostics.py +180 -88
- mcp_ticketer/cli/linear_commands.py +156 -113
- mcp_ticketer/cli/main.py +153 -82
- mcp_ticketer/cli/simple_health.py +74 -51
- mcp_ticketer/cli/utils.py +15 -10
- mcp_ticketer/core/config.py +23 -19
- mcp_ticketer/core/env_discovery.py +5 -4
- mcp_ticketer/core/env_loader.py +114 -91
- mcp_ticketer/core/exceptions.py +22 -20
- mcp_ticketer/core/models.py +9 -0
- mcp_ticketer/core/project_config.py +1 -1
- mcp_ticketer/mcp/constants.py +58 -0
- mcp_ticketer/mcp/dto.py +195 -0
- mcp_ticketer/mcp/response_builder.py +206 -0
- mcp_ticketer/mcp/server.py +361 -1182
- mcp_ticketer/queue/health_monitor.py +166 -135
- mcp_ticketer/queue/manager.py +70 -19
- mcp_ticketer/queue/queue.py +24 -5
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +203 -145
- mcp_ticketer/queue/worker.py +79 -43
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/METADATA +1 -1
- mcp_ticketer-0.3.3.dist-info/RECORD +62 -0
- mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.3.dist-info}/top_level.txt +0 -0
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import asyncio
|
|
6
5
|
import os
|
|
7
|
-
from
|
|
8
|
-
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
from typing import Any
|
|
9
7
|
|
|
10
8
|
try:
|
|
11
9
|
from gql import gql
|
|
@@ -14,23 +12,15 @@ except ImportError:
|
|
|
14
12
|
gql = None
|
|
15
13
|
TransportQueryError = Exception
|
|
16
14
|
|
|
15
|
+
import builtins
|
|
16
|
+
|
|
17
17
|
from ...core.adapter import BaseAdapter
|
|
18
|
-
from ...core.models import
|
|
19
|
-
Comment,
|
|
20
|
-
Epic,
|
|
21
|
-
Priority,
|
|
22
|
-
SearchQuery,
|
|
23
|
-
Task,
|
|
24
|
-
TicketState,
|
|
25
|
-
TicketType,
|
|
26
|
-
)
|
|
18
|
+
from ...core.models import Comment, Epic, SearchQuery, Task, TicketState
|
|
27
19
|
from ...core.registry import AdapterRegistry
|
|
28
|
-
|
|
29
20
|
from .client import LinearGraphQLClient
|
|
30
21
|
from .mappers import (
|
|
31
22
|
build_linear_issue_input,
|
|
32
23
|
build_linear_issue_update_input,
|
|
33
|
-
extract_child_issue_ids,
|
|
34
24
|
map_linear_comment_to_comment,
|
|
35
25
|
map_linear_issue_to_task,
|
|
36
26
|
map_linear_project_to_epic,
|
|
@@ -38,7 +28,6 @@ from .mappers import (
|
|
|
38
28
|
from .queries import (
|
|
39
29
|
ALL_FRAGMENTS,
|
|
40
30
|
CREATE_ISSUE_MUTATION,
|
|
41
|
-
GET_CURRENT_USER_QUERY,
|
|
42
31
|
LIST_ISSUES_QUERY,
|
|
43
32
|
SEARCH_ISSUES_QUERY,
|
|
44
33
|
UPDATE_ISSUE_MUTATION,
|
|
@@ -49,22 +38,21 @@ from .types import (
|
|
|
49
38
|
build_issue_filter,
|
|
50
39
|
get_linear_priority,
|
|
51
40
|
get_linear_state_type,
|
|
52
|
-
get_universal_state,
|
|
53
41
|
)
|
|
54
42
|
|
|
55
43
|
|
|
56
44
|
class LinearAdapter(BaseAdapter[Task]):
|
|
57
45
|
"""Adapter for Linear issue tracking system using native GraphQL API.
|
|
58
|
-
|
|
46
|
+
|
|
59
47
|
This adapter provides comprehensive integration with Linear's GraphQL API,
|
|
60
48
|
supporting all major ticket management operations including:
|
|
61
|
-
|
|
49
|
+
|
|
62
50
|
- CRUD operations for issues and projects
|
|
63
51
|
- State transitions and workflow management
|
|
64
52
|
- User assignment and search functionality
|
|
65
53
|
- Comment management
|
|
66
54
|
- Epic/Issue/Task hierarchy support
|
|
67
|
-
|
|
55
|
+
|
|
68
56
|
The adapter is organized into multiple modules for better maintainability:
|
|
69
57
|
- client.py: GraphQL client management
|
|
70
58
|
- queries.py: GraphQL queries and fragments
|
|
@@ -72,7 +60,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
72
60
|
- mappers.py: Data transformation logic
|
|
73
61
|
"""
|
|
74
62
|
|
|
75
|
-
def __init__(self, config:
|
|
63
|
+
def __init__(self, config: dict[str, Any]):
|
|
76
64
|
"""Initialize Linear adapter.
|
|
77
65
|
|
|
78
66
|
Args:
|
|
@@ -85,89 +73,94 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
85
73
|
|
|
86
74
|
Raises:
|
|
87
75
|
ValueError: If required configuration is missing
|
|
76
|
+
|
|
88
77
|
"""
|
|
89
78
|
# Initialize instance variables before calling super().__init__
|
|
90
79
|
# because parent constructor calls _get_state_mapping()
|
|
91
|
-
self._team_data:
|
|
92
|
-
self._workflow_states:
|
|
93
|
-
self._labels_cache:
|
|
94
|
-
self._users_cache:
|
|
80
|
+
self._team_data: dict[str, Any] | None = None
|
|
81
|
+
self._workflow_states: dict[str, dict[str, Any]] | None = None
|
|
82
|
+
self._labels_cache: list[dict[str, Any]] | None = None
|
|
83
|
+
self._users_cache: dict[str, dict[str, Any]] | None = None
|
|
95
84
|
self._initialized = False
|
|
96
85
|
|
|
97
86
|
super().__init__(config)
|
|
98
|
-
|
|
87
|
+
|
|
99
88
|
# Extract configuration
|
|
100
89
|
self.api_key = config.get("api_key") or os.getenv("LINEAR_API_KEY")
|
|
101
90
|
if not self.api_key:
|
|
102
|
-
raise ValueError(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"Linear API key is required (api_key or LINEAR_API_KEY env var)"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Clean API key - remove Bearer prefix if accidentally included in config
|
|
96
|
+
# (The client will add it back when making requests)
|
|
97
|
+
if self.api_key.startswith("Bearer "):
|
|
98
|
+
self.api_key = self.api_key.replace("Bearer ", "")
|
|
99
|
+
|
|
108
100
|
self.workspace = config.get("workspace", "")
|
|
109
101
|
self.team_key = config.get("team_key")
|
|
110
102
|
self.team_id = config.get("team_id")
|
|
111
103
|
self.api_url = config.get("api_url", "https://api.linear.app/graphql")
|
|
112
|
-
|
|
104
|
+
|
|
113
105
|
# Validate team configuration
|
|
114
106
|
if not self.team_key and not self.team_id:
|
|
115
107
|
raise ValueError("Either team_key or team_id must be provided")
|
|
116
|
-
|
|
117
|
-
# Initialize client
|
|
118
|
-
|
|
119
|
-
self.client = LinearGraphQLClient(api_key_clean)
|
|
108
|
+
|
|
109
|
+
# Initialize client with clean API key
|
|
110
|
+
self.client = LinearGraphQLClient(self.api_key)
|
|
120
111
|
|
|
121
112
|
def validate_credentials(self) -> tuple[bool, str]:
|
|
122
113
|
"""Validate Linear API credentials.
|
|
123
|
-
|
|
114
|
+
|
|
124
115
|
Returns:
|
|
125
116
|
Tuple of (is_valid, error_message)
|
|
117
|
+
|
|
126
118
|
"""
|
|
127
119
|
if not self.api_key:
|
|
128
120
|
return False, "Linear API key is required"
|
|
129
|
-
|
|
121
|
+
|
|
130
122
|
if not self.team_key and not self.team_id:
|
|
131
123
|
return False, "Either team_key or team_id must be provided"
|
|
132
|
-
|
|
124
|
+
|
|
133
125
|
return True, ""
|
|
134
126
|
|
|
135
127
|
async def initialize(self) -> None:
|
|
136
128
|
"""Initialize adapter by preloading team, states, and labels data concurrently."""
|
|
137
129
|
if self._initialized:
|
|
138
130
|
return
|
|
139
|
-
|
|
131
|
+
|
|
140
132
|
try:
|
|
141
133
|
# Test connection first
|
|
142
134
|
if not await self.client.test_connection():
|
|
143
135
|
raise ValueError("Failed to connect to Linear API - check credentials")
|
|
144
|
-
|
|
136
|
+
|
|
145
137
|
# Load team data and workflow states concurrently
|
|
146
138
|
team_id = await self._ensure_team_id()
|
|
147
|
-
|
|
139
|
+
|
|
148
140
|
# Load workflow states for the team
|
|
149
141
|
await self._load_workflow_states(team_id)
|
|
150
|
-
|
|
142
|
+
|
|
151
143
|
self._initialized = True
|
|
152
|
-
|
|
144
|
+
|
|
153
145
|
except Exception as e:
|
|
154
146
|
raise ValueError(f"Failed to initialize Linear adapter: {e}")
|
|
155
147
|
|
|
156
148
|
async def _ensure_team_id(self) -> str:
|
|
157
149
|
"""Ensure we have a team ID, resolving from team_key if needed.
|
|
158
|
-
|
|
150
|
+
|
|
159
151
|
Returns:
|
|
160
152
|
Linear team UUID
|
|
161
|
-
|
|
153
|
+
|
|
162
154
|
Raises:
|
|
163
155
|
ValueError: If team cannot be found or resolved
|
|
156
|
+
|
|
164
157
|
"""
|
|
165
158
|
if self.team_id:
|
|
166
159
|
return self.team_id
|
|
167
|
-
|
|
160
|
+
|
|
168
161
|
if not self.team_key:
|
|
169
162
|
raise ValueError("Either team_id or team_key must be provided")
|
|
170
|
-
|
|
163
|
+
|
|
171
164
|
# Query team by key
|
|
172
165
|
query = """
|
|
173
166
|
query GetTeamByKey($key: String!) {
|
|
@@ -181,35 +174,35 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
181
174
|
}
|
|
182
175
|
}
|
|
183
176
|
"""
|
|
184
|
-
|
|
177
|
+
|
|
185
178
|
try:
|
|
186
179
|
result = await self.client.execute_query(query, {"key": self.team_key})
|
|
187
180
|
teams = result.get("teams", {}).get("nodes", [])
|
|
188
|
-
|
|
181
|
+
|
|
189
182
|
if not teams:
|
|
190
183
|
raise ValueError(f"Team with key '{self.team_key}' not found")
|
|
191
|
-
|
|
184
|
+
|
|
192
185
|
team = teams[0]
|
|
193
186
|
self.team_id = team["id"]
|
|
194
187
|
self._team_data = team
|
|
195
|
-
|
|
188
|
+
|
|
196
189
|
return self.team_id
|
|
197
|
-
|
|
190
|
+
|
|
198
191
|
except Exception as e:
|
|
199
192
|
raise ValueError(f"Failed to resolve team '{self.team_key}': {e}")
|
|
200
193
|
|
|
201
194
|
async def _load_workflow_states(self, team_id: str) -> None:
|
|
202
195
|
"""Load and cache workflow states for the team.
|
|
203
|
-
|
|
196
|
+
|
|
204
197
|
Args:
|
|
205
198
|
team_id: Linear team ID
|
|
199
|
+
|
|
206
200
|
"""
|
|
207
201
|
try:
|
|
208
202
|
result = await self.client.execute_query(
|
|
209
|
-
WORKFLOW_STATES_QUERY,
|
|
210
|
-
{"teamId": team_id}
|
|
203
|
+
WORKFLOW_STATES_QUERY, {"teamId": team_id}
|
|
211
204
|
)
|
|
212
|
-
|
|
205
|
+
|
|
213
206
|
workflow_states = {}
|
|
214
207
|
for state in result["workflowStates"]["nodes"]:
|
|
215
208
|
state_type = state["type"].lower()
|
|
@@ -217,23 +210,24 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
217
210
|
workflow_states[state_type] = state
|
|
218
211
|
elif state["position"] < workflow_states[state_type]["position"]:
|
|
219
212
|
workflow_states[state_type] = state
|
|
220
|
-
|
|
213
|
+
|
|
221
214
|
self._workflow_states = workflow_states
|
|
222
|
-
|
|
215
|
+
|
|
223
216
|
except Exception as e:
|
|
224
217
|
raise ValueError(f"Failed to load workflow states: {e}")
|
|
225
218
|
|
|
226
|
-
def _get_state_mapping(self) ->
|
|
219
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
227
220
|
"""Get mapping from universal states to Linear workflow state IDs.
|
|
228
|
-
|
|
221
|
+
|
|
229
222
|
Returns:
|
|
230
223
|
Dictionary mapping TicketState to Linear state ID
|
|
224
|
+
|
|
231
225
|
"""
|
|
232
226
|
if not self._workflow_states:
|
|
233
227
|
# Return type-based mapping if states not loaded
|
|
234
228
|
return {
|
|
235
229
|
TicketState.OPEN: "unstarted",
|
|
236
|
-
TicketState.IN_PROGRESS: "started",
|
|
230
|
+
TicketState.IN_PROGRESS: "started",
|
|
237
231
|
TicketState.READY: "unstarted",
|
|
238
232
|
TicketState.TESTED: "started",
|
|
239
233
|
TicketState.DONE: "completed",
|
|
@@ -241,7 +235,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
241
235
|
TicketState.WAITING: "unstarted",
|
|
242
236
|
TicketState.BLOCKED: "unstarted",
|
|
243
237
|
}
|
|
244
|
-
|
|
238
|
+
|
|
245
239
|
# Return ID-based mapping using cached workflow states
|
|
246
240
|
mapping = {}
|
|
247
241
|
for universal_state, linear_type in LinearStateMapping.TO_LINEAR.items():
|
|
@@ -250,30 +244,31 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
250
244
|
else:
|
|
251
245
|
# Fallback to type name
|
|
252
246
|
mapping[universal_state] = linear_type
|
|
253
|
-
|
|
247
|
+
|
|
254
248
|
return mapping
|
|
255
249
|
|
|
256
|
-
async def _get_user_id(self, user_identifier: str) ->
|
|
250
|
+
async def _get_user_id(self, user_identifier: str) -> str | None:
|
|
257
251
|
"""Get Linear user ID from email or display name.
|
|
258
|
-
|
|
252
|
+
|
|
259
253
|
Args:
|
|
260
254
|
user_identifier: Email address or display name
|
|
261
|
-
|
|
255
|
+
|
|
262
256
|
Returns:
|
|
263
257
|
Linear user ID or None if not found
|
|
258
|
+
|
|
264
259
|
"""
|
|
265
260
|
# Try to get user by email first
|
|
266
261
|
user = await self.client.get_user_by_email(user_identifier)
|
|
267
262
|
if user:
|
|
268
263
|
return user["id"]
|
|
269
|
-
|
|
264
|
+
|
|
270
265
|
# If not found by email, could implement search by display name
|
|
271
266
|
# For now, assume the identifier is already a user ID
|
|
272
267
|
return user_identifier if user_identifier else None
|
|
273
268
|
|
|
274
269
|
# CRUD Operations
|
|
275
270
|
|
|
276
|
-
async def create(self, ticket:
|
|
271
|
+
async def create(self, ticket: Epic | Task) -> Epic | Task:
|
|
277
272
|
"""Create a new Linear issue or project with full field support.
|
|
278
273
|
|
|
279
274
|
Args:
|
|
@@ -284,6 +279,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
284
279
|
|
|
285
280
|
Raises:
|
|
286
281
|
ValueError: If credentials are invalid or creation fails
|
|
282
|
+
|
|
287
283
|
"""
|
|
288
284
|
# Validate credentials before attempting operation
|
|
289
285
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -308,6 +304,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
308
304
|
|
|
309
305
|
Returns:
|
|
310
306
|
Created task with Linear metadata
|
|
307
|
+
|
|
311
308
|
"""
|
|
312
309
|
team_id = await self._ensure_team_id()
|
|
313
310
|
|
|
@@ -322,8 +319,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
322
319
|
|
|
323
320
|
try:
|
|
324
321
|
result = await self.client.execute_mutation(
|
|
325
|
-
CREATE_ISSUE_MUTATION,
|
|
326
|
-
{"input": issue_input}
|
|
322
|
+
CREATE_ISSUE_MUTATION, {"input": issue_input}
|
|
327
323
|
)
|
|
328
324
|
|
|
329
325
|
if not result["issueCreate"]["success"]:
|
|
@@ -343,6 +339,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
343
339
|
|
|
344
340
|
Returns:
|
|
345
341
|
Created epic with Linear metadata
|
|
342
|
+
|
|
346
343
|
"""
|
|
347
344
|
team_id = await self._ensure_team_id()
|
|
348
345
|
|
|
@@ -387,8 +384,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
387
384
|
|
|
388
385
|
try:
|
|
389
386
|
result = await self.client.execute_mutation(
|
|
390
|
-
create_query,
|
|
391
|
-
{"input": project_input}
|
|
387
|
+
create_query, {"input": project_input}
|
|
392
388
|
)
|
|
393
389
|
|
|
394
390
|
if not result["projectCreate"]["success"]:
|
|
@@ -400,7 +396,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
400
396
|
except Exception as e:
|
|
401
397
|
raise ValueError(f"Failed to create Linear project: {e}")
|
|
402
398
|
|
|
403
|
-
async def read(self, ticket_id: str) ->
|
|
399
|
+
async def read(self, ticket_id: str) -> Task | None:
|
|
404
400
|
"""Read a Linear issue by identifier with full details.
|
|
405
401
|
|
|
406
402
|
Args:
|
|
@@ -408,25 +404,26 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
408
404
|
|
|
409
405
|
Returns:
|
|
410
406
|
Task with full details or None if not found
|
|
407
|
+
|
|
411
408
|
"""
|
|
412
409
|
# Validate credentials before attempting operation
|
|
413
410
|
is_valid, error_message = self.validate_credentials()
|
|
414
411
|
if not is_valid:
|
|
415
412
|
raise ValueError(error_message)
|
|
416
413
|
|
|
417
|
-
query =
|
|
414
|
+
query = (
|
|
415
|
+
ALL_FRAGMENTS
|
|
416
|
+
+ """
|
|
418
417
|
query GetIssue($identifier: String!) {
|
|
419
418
|
issue(id: $identifier) {
|
|
420
419
|
...IssueFullFields
|
|
421
420
|
}
|
|
422
421
|
}
|
|
423
422
|
"""
|
|
423
|
+
)
|
|
424
424
|
|
|
425
425
|
try:
|
|
426
|
-
result = await self.client.execute_query(
|
|
427
|
-
query,
|
|
428
|
-
{"identifier": ticket_id}
|
|
429
|
-
)
|
|
426
|
+
result = await self.client.execute_query(query, {"identifier": ticket_id})
|
|
430
427
|
|
|
431
428
|
if result.get("issue"):
|
|
432
429
|
return map_linear_issue_to_task(result["issue"])
|
|
@@ -437,7 +434,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
437
434
|
|
|
438
435
|
return None
|
|
439
436
|
|
|
440
|
-
async def update(self, ticket_id: str, updates:
|
|
437
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> Task | None:
|
|
441
438
|
"""Update a Linear issue with comprehensive field support.
|
|
442
439
|
|
|
443
440
|
Args:
|
|
@@ -446,6 +443,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
446
443
|
|
|
447
444
|
Returns:
|
|
448
445
|
Updated task or None if not found
|
|
446
|
+
|
|
449
447
|
"""
|
|
450
448
|
# Validate credentials before attempting operation
|
|
451
449
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -463,8 +461,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
463
461
|
|
|
464
462
|
try:
|
|
465
463
|
result = await self.client.execute_query(
|
|
466
|
-
id_query,
|
|
467
|
-
{"identifier": ticket_id}
|
|
464
|
+
id_query, {"identifier": ticket_id}
|
|
468
465
|
)
|
|
469
466
|
|
|
470
467
|
if not result.get("issue"):
|
|
@@ -477,7 +474,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
477
474
|
|
|
478
475
|
# Handle state transitions
|
|
479
476
|
if "state" in updates:
|
|
480
|
-
target_state =
|
|
477
|
+
target_state = (
|
|
478
|
+
TicketState(updates["state"])
|
|
479
|
+
if isinstance(updates["state"], str)
|
|
480
|
+
else updates["state"]
|
|
481
|
+
)
|
|
481
482
|
state_mapping = self._get_state_mapping()
|
|
482
483
|
if target_state in state_mapping:
|
|
483
484
|
update_input["stateId"] = state_mapping[target_state]
|
|
@@ -490,8 +491,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
490
491
|
|
|
491
492
|
# Execute update
|
|
492
493
|
result = await self.client.execute_mutation(
|
|
493
|
-
UPDATE_ISSUE_MUTATION,
|
|
494
|
-
{"id": linear_id, "input": update_input}
|
|
494
|
+
UPDATE_ISSUE_MUTATION, {"id": linear_id, "input": update_input}
|
|
495
495
|
)
|
|
496
496
|
|
|
497
497
|
if not result["issueUpdate"]["success"]:
|
|
@@ -511,6 +511,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
511
511
|
|
|
512
512
|
Returns:
|
|
513
513
|
True if successfully deleted/archived
|
|
514
|
+
|
|
514
515
|
"""
|
|
515
516
|
# Linear doesn't support true deletion, so we archive the issue
|
|
516
517
|
try:
|
|
@@ -520,11 +521,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
520
521
|
return False
|
|
521
522
|
|
|
522
523
|
async def list(
|
|
523
|
-
self,
|
|
524
|
-
|
|
525
|
-
offset: int = 0,
|
|
526
|
-
filters: Optional[Dict[str, Any]] = None
|
|
527
|
-
) -> List[Task]:
|
|
524
|
+
self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None
|
|
525
|
+
) -> builtins.list[Task]:
|
|
528
526
|
"""List Linear issues with optional filtering.
|
|
529
527
|
|
|
530
528
|
Args:
|
|
@@ -534,6 +532,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
534
532
|
|
|
535
533
|
Returns:
|
|
536
534
|
List of tasks matching the criteria
|
|
535
|
+
|
|
537
536
|
"""
|
|
538
537
|
# Validate credentials
|
|
539
538
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -548,7 +547,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
548
547
|
team_id=team_id,
|
|
549
548
|
state=filters.get("state") if filters else None,
|
|
550
549
|
priority=filters.get("priority") if filters else None,
|
|
551
|
-
include_archived=
|
|
550
|
+
include_archived=(
|
|
551
|
+
filters.get("includeArchived", False) if filters else False
|
|
552
|
+
),
|
|
552
553
|
)
|
|
553
554
|
|
|
554
555
|
# Add additional filters
|
|
@@ -567,8 +568,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
567
568
|
|
|
568
569
|
try:
|
|
569
570
|
result = await self.client.execute_query(
|
|
570
|
-
LIST_ISSUES_QUERY,
|
|
571
|
-
{"filter": issue_filter, "first": limit}
|
|
571
|
+
LIST_ISSUES_QUERY, {"filter": issue_filter, "first": limit}
|
|
572
572
|
)
|
|
573
573
|
|
|
574
574
|
tasks = []
|
|
@@ -580,7 +580,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
580
580
|
except Exception as e:
|
|
581
581
|
raise ValueError(f"Failed to list Linear issues: {e}")
|
|
582
582
|
|
|
583
|
-
async def search(self, query: SearchQuery) ->
|
|
583
|
+
async def search(self, query: SearchQuery) -> builtins.list[Task]:
|
|
584
584
|
"""Search Linear issues using comprehensive filters.
|
|
585
585
|
|
|
586
586
|
Args:
|
|
@@ -588,6 +588,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
588
588
|
|
|
589
589
|
Returns:
|
|
590
590
|
List of tasks matching the search criteria
|
|
591
|
+
|
|
591
592
|
"""
|
|
592
593
|
# Validate credentials
|
|
593
594
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -631,8 +632,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
631
632
|
|
|
632
633
|
try:
|
|
633
634
|
result = await self.client.execute_query(
|
|
634
|
-
SEARCH_ISSUES_QUERY,
|
|
635
|
-
{"filter": issue_filter, "first": query.limit}
|
|
635
|
+
SEARCH_ISSUES_QUERY, {"filter": issue_filter, "first": query.limit}
|
|
636
636
|
)
|
|
637
637
|
|
|
638
638
|
tasks = []
|
|
@@ -646,7 +646,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
646
646
|
|
|
647
647
|
async def transition_state(
|
|
648
648
|
self, ticket_id: str, target_state: TicketState
|
|
649
|
-
) ->
|
|
649
|
+
) -> Task | None:
|
|
650
650
|
"""Transition Linear issue to new state with workflow validation.
|
|
651
651
|
|
|
652
652
|
Args:
|
|
@@ -655,6 +655,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
655
655
|
|
|
656
656
|
Returns:
|
|
657
657
|
Updated task or None if transition failed
|
|
658
|
+
|
|
658
659
|
"""
|
|
659
660
|
# Validate transition
|
|
660
661
|
if not await self.validate_transition(ticket_id, target_state):
|
|
@@ -674,6 +675,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
674
675
|
|
|
675
676
|
Returns:
|
|
676
677
|
True if transition is valid
|
|
678
|
+
|
|
677
679
|
"""
|
|
678
680
|
# For now, allow all transitions
|
|
679
681
|
# In practice, you might want to implement Linear's workflow rules
|
|
@@ -687,6 +689,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
687
689
|
|
|
688
690
|
Returns:
|
|
689
691
|
Created comment with ID
|
|
692
|
+
|
|
690
693
|
"""
|
|
691
694
|
# First get the Linear internal ID
|
|
692
695
|
id_query = """
|
|
@@ -699,8 +702,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
699
702
|
|
|
700
703
|
try:
|
|
701
704
|
result = await self.client.execute_query(
|
|
702
|
-
id_query,
|
|
703
|
-
{"identifier": comment.ticket_id}
|
|
705
|
+
id_query, {"identifier": comment.ticket_id}
|
|
704
706
|
)
|
|
705
707
|
|
|
706
708
|
if not result.get("issue"):
|
|
@@ -735,8 +737,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
735
737
|
}
|
|
736
738
|
|
|
737
739
|
result = await self.client.execute_mutation(
|
|
738
|
-
create_comment_query,
|
|
739
|
-
{"input": comment_input}
|
|
740
|
+
create_comment_query, {"input": comment_input}
|
|
740
741
|
)
|
|
741
742
|
|
|
742
743
|
if not result["commentCreate"]["success"]:
|
|
@@ -750,7 +751,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
750
751
|
|
|
751
752
|
async def get_comments(
|
|
752
753
|
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
753
|
-
) ->
|
|
754
|
+
) -> builtins.list[Comment]:
|
|
754
755
|
"""Get comments for a Linear issue.
|
|
755
756
|
|
|
756
757
|
Args:
|
|
@@ -760,6 +761,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
760
761
|
|
|
761
762
|
Returns:
|
|
762
763
|
List of comments for the issue
|
|
764
|
+
|
|
763
765
|
"""
|
|
764
766
|
query = """
|
|
765
767
|
query GetIssueComments($identifier: String!, $first: Int!) {
|
|
@@ -788,8 +790,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
788
790
|
|
|
789
791
|
try:
|
|
790
792
|
result = await self.client.execute_query(
|
|
791
|
-
query,
|
|
792
|
-
{"identifier": ticket_id, "first": limit}
|
|
793
|
+
query, {"identifier": ticket_id, "first": limit}
|
|
793
794
|
)
|
|
794
795
|
|
|
795
796
|
if not result.get("issue"):
|