mcp-ticketer 0.1.39__py3-none-any.whl → 0.3.0__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/__version__.py +1 -1
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +813 -0
- mcp_ticketer/adapters/linear/client.py +257 -0
- mcp_ticketer/adapters/linear/mappers.py +307 -0
- mcp_ticketer/adapters/linear/queries.py +366 -0
- mcp_ticketer/adapters/linear/types.py +277 -0
- mcp_ticketer/adapters/linear.py +10 -2384
- mcp_ticketer/cli/adapter_diagnostics.py +384 -0
- mcp_ticketer/cli/main.py +327 -67
- mcp_ticketer/core/exceptions.py +152 -0
- mcp_ticketer/mcp/server.py +172 -37
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/RECORD +18 -10
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.39.dist-info → mcp_ticketer-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""GraphQL queries and fragments for Linear API."""
|
|
2
|
+
|
|
3
|
+
# GraphQL Fragments for reusable field definitions
|
|
4
|
+
|
|
5
|
+
USER_FRAGMENT = """
|
|
6
|
+
fragment UserFields on User {
|
|
7
|
+
id
|
|
8
|
+
name
|
|
9
|
+
email
|
|
10
|
+
displayName
|
|
11
|
+
avatarUrl
|
|
12
|
+
isMe
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
WORKFLOW_STATE_FRAGMENT = """
|
|
17
|
+
fragment WorkflowStateFields on WorkflowState {
|
|
18
|
+
id
|
|
19
|
+
name
|
|
20
|
+
type
|
|
21
|
+
position
|
|
22
|
+
color
|
|
23
|
+
}
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
TEAM_FRAGMENT = """
|
|
27
|
+
fragment TeamFields on Team {
|
|
28
|
+
id
|
|
29
|
+
name
|
|
30
|
+
key
|
|
31
|
+
description
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
CYCLE_FRAGMENT = """
|
|
36
|
+
fragment CycleFields on Cycle {
|
|
37
|
+
id
|
|
38
|
+
number
|
|
39
|
+
name
|
|
40
|
+
description
|
|
41
|
+
startsAt
|
|
42
|
+
endsAt
|
|
43
|
+
completedAt
|
|
44
|
+
}
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
PROJECT_FRAGMENT = """
|
|
48
|
+
fragment ProjectFields on Project {
|
|
49
|
+
id
|
|
50
|
+
name
|
|
51
|
+
description
|
|
52
|
+
state
|
|
53
|
+
createdAt
|
|
54
|
+
updatedAt
|
|
55
|
+
url
|
|
56
|
+
icon
|
|
57
|
+
color
|
|
58
|
+
targetDate
|
|
59
|
+
startedAt
|
|
60
|
+
completedAt
|
|
61
|
+
teams {
|
|
62
|
+
nodes {
|
|
63
|
+
...TeamFields
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
LABEL_FRAGMENT = """
|
|
70
|
+
fragment LabelFields on IssueLabel {
|
|
71
|
+
id
|
|
72
|
+
name
|
|
73
|
+
color
|
|
74
|
+
description
|
|
75
|
+
}
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
ATTACHMENT_FRAGMENT = """
|
|
79
|
+
fragment AttachmentFields on Attachment {
|
|
80
|
+
id
|
|
81
|
+
title
|
|
82
|
+
url
|
|
83
|
+
subtitle
|
|
84
|
+
metadata
|
|
85
|
+
createdAt
|
|
86
|
+
updatedAt
|
|
87
|
+
}
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
COMMENT_FRAGMENT = """
|
|
91
|
+
fragment CommentFields on Comment {
|
|
92
|
+
id
|
|
93
|
+
body
|
|
94
|
+
createdAt
|
|
95
|
+
updatedAt
|
|
96
|
+
user {
|
|
97
|
+
...UserFields
|
|
98
|
+
}
|
|
99
|
+
parent {
|
|
100
|
+
id
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
ISSUE_COMPACT_FRAGMENT = """
|
|
106
|
+
fragment IssueCompactFields on Issue {
|
|
107
|
+
id
|
|
108
|
+
identifier
|
|
109
|
+
title
|
|
110
|
+
description
|
|
111
|
+
priority
|
|
112
|
+
priorityLabel
|
|
113
|
+
estimate
|
|
114
|
+
dueDate
|
|
115
|
+
slaBreachesAt
|
|
116
|
+
slaStartedAt
|
|
117
|
+
createdAt
|
|
118
|
+
updatedAt
|
|
119
|
+
archivedAt
|
|
120
|
+
canceledAt
|
|
121
|
+
completedAt
|
|
122
|
+
startedAt
|
|
123
|
+
startedTriageAt
|
|
124
|
+
triagedAt
|
|
125
|
+
url
|
|
126
|
+
branchName
|
|
127
|
+
customerTicketCount
|
|
128
|
+
|
|
129
|
+
state {
|
|
130
|
+
...WorkflowStateFields
|
|
131
|
+
}
|
|
132
|
+
assignee {
|
|
133
|
+
...UserFields
|
|
134
|
+
}
|
|
135
|
+
creator {
|
|
136
|
+
...UserFields
|
|
137
|
+
}
|
|
138
|
+
labels {
|
|
139
|
+
nodes {
|
|
140
|
+
...LabelFields
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
team {
|
|
144
|
+
...TeamFields
|
|
145
|
+
}
|
|
146
|
+
cycle {
|
|
147
|
+
...CycleFields
|
|
148
|
+
}
|
|
149
|
+
project {
|
|
150
|
+
...ProjectFields
|
|
151
|
+
}
|
|
152
|
+
parent {
|
|
153
|
+
id
|
|
154
|
+
identifier
|
|
155
|
+
title
|
|
156
|
+
}
|
|
157
|
+
children {
|
|
158
|
+
nodes {
|
|
159
|
+
id
|
|
160
|
+
identifier
|
|
161
|
+
title
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
attachments {
|
|
165
|
+
nodes {
|
|
166
|
+
...AttachmentFields
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
ISSUE_FULL_FRAGMENT = """
|
|
173
|
+
fragment IssueFullFields on Issue {
|
|
174
|
+
...IssueCompactFields
|
|
175
|
+
comments {
|
|
176
|
+
nodes {
|
|
177
|
+
...CommentFields
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
subscribers {
|
|
181
|
+
nodes {
|
|
182
|
+
...UserFields
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
relations {
|
|
186
|
+
nodes {
|
|
187
|
+
id
|
|
188
|
+
type
|
|
189
|
+
relatedIssue {
|
|
190
|
+
id
|
|
191
|
+
identifier
|
|
192
|
+
title
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
# Combine all fragments
|
|
200
|
+
ALL_FRAGMENTS = (
|
|
201
|
+
USER_FRAGMENT
|
|
202
|
+
+ WORKFLOW_STATE_FRAGMENT
|
|
203
|
+
+ TEAM_FRAGMENT
|
|
204
|
+
+ CYCLE_FRAGMENT
|
|
205
|
+
+ PROJECT_FRAGMENT
|
|
206
|
+
+ LABEL_FRAGMENT
|
|
207
|
+
+ ATTACHMENT_FRAGMENT
|
|
208
|
+
+ COMMENT_FRAGMENT
|
|
209
|
+
+ ISSUE_COMPACT_FRAGMENT
|
|
210
|
+
+ ISSUE_FULL_FRAGMENT
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Fragments needed for issue list/search (without comments)
|
|
214
|
+
ISSUE_LIST_FRAGMENTS = (
|
|
215
|
+
USER_FRAGMENT
|
|
216
|
+
+ WORKFLOW_STATE_FRAGMENT
|
|
217
|
+
+ TEAM_FRAGMENT
|
|
218
|
+
+ CYCLE_FRAGMENT
|
|
219
|
+
+ PROJECT_FRAGMENT
|
|
220
|
+
+ LABEL_FRAGMENT
|
|
221
|
+
+ ATTACHMENT_FRAGMENT
|
|
222
|
+
+ ISSUE_COMPACT_FRAGMENT
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Query definitions
|
|
226
|
+
|
|
227
|
+
WORKFLOW_STATES_QUERY = """
|
|
228
|
+
query WorkflowStates($teamId: ID!) {
|
|
229
|
+
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
230
|
+
nodes {
|
|
231
|
+
id
|
|
232
|
+
name
|
|
233
|
+
type
|
|
234
|
+
position
|
|
235
|
+
color
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
CREATE_ISSUE_MUTATION = ALL_FRAGMENTS + """
|
|
242
|
+
mutation CreateIssue($input: IssueCreateInput!) {
|
|
243
|
+
issueCreate(input: $input) {
|
|
244
|
+
success
|
|
245
|
+
issue {
|
|
246
|
+
...IssueFullFields
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
UPDATE_ISSUE_MUTATION = ALL_FRAGMENTS + """
|
|
253
|
+
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
254
|
+
issueUpdate(id: $id, input: $input) {
|
|
255
|
+
success
|
|
256
|
+
issue {
|
|
257
|
+
...IssueFullFields
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
"""
|
|
262
|
+
|
|
263
|
+
LIST_ISSUES_QUERY = ISSUE_LIST_FRAGMENTS + """
|
|
264
|
+
query ListIssues($filter: IssueFilter, $first: Int!) {
|
|
265
|
+
issues(
|
|
266
|
+
filter: $filter
|
|
267
|
+
first: $first
|
|
268
|
+
orderBy: updatedAt
|
|
269
|
+
) {
|
|
270
|
+
nodes {
|
|
271
|
+
...IssueCompactFields
|
|
272
|
+
}
|
|
273
|
+
pageInfo {
|
|
274
|
+
hasNextPage
|
|
275
|
+
hasPreviousPage
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
SEARCH_ISSUES_QUERY = ISSUE_LIST_FRAGMENTS + """
|
|
282
|
+
query SearchIssues($filter: IssueFilter, $first: Int!) {
|
|
283
|
+
issues(
|
|
284
|
+
filter: $filter
|
|
285
|
+
first: $first
|
|
286
|
+
orderBy: updatedAt
|
|
287
|
+
) {
|
|
288
|
+
nodes {
|
|
289
|
+
...IssueCompactFields
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
GET_CYCLES_QUERY = """
|
|
296
|
+
query GetCycles($filter: CycleFilter) {
|
|
297
|
+
cycles(filter: $filter, orderBy: createdAt) {
|
|
298
|
+
nodes {
|
|
299
|
+
id
|
|
300
|
+
number
|
|
301
|
+
name
|
|
302
|
+
description
|
|
303
|
+
startsAt
|
|
304
|
+
endsAt
|
|
305
|
+
completedAt
|
|
306
|
+
issues {
|
|
307
|
+
nodes {
|
|
308
|
+
id
|
|
309
|
+
identifier
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
UPDATE_ISSUE_BRANCH_MUTATION = """
|
|
318
|
+
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
319
|
+
issueUpdate(id: $id, input: $input) {
|
|
320
|
+
issue {
|
|
321
|
+
id
|
|
322
|
+
identifier
|
|
323
|
+
branchName
|
|
324
|
+
}
|
|
325
|
+
success
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
SEARCH_ISSUE_BY_IDENTIFIER_QUERY = """
|
|
331
|
+
query SearchIssue($identifier: String!) {
|
|
332
|
+
issue(id: $identifier) {
|
|
333
|
+
id
|
|
334
|
+
identifier
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
LIST_PROJECTS_QUERY = PROJECT_FRAGMENT + """
|
|
340
|
+
query ListProjects($filter: ProjectFilter, $first: Int!) {
|
|
341
|
+
projects(filter: $filter, first: $first, orderBy: updatedAt) {
|
|
342
|
+
nodes {
|
|
343
|
+
...ProjectFields
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
CREATE_SUB_ISSUE_MUTATION = ALL_FRAGMENTS + """
|
|
350
|
+
mutation CreateSubIssue($input: IssueCreateInput!) {
|
|
351
|
+
issueCreate(input: $input) {
|
|
352
|
+
success
|
|
353
|
+
issue {
|
|
354
|
+
...IssueFullFields
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
"""
|
|
359
|
+
|
|
360
|
+
GET_CURRENT_USER_QUERY = USER_FRAGMENT + """
|
|
361
|
+
query GetCurrentUser {
|
|
362
|
+
viewer {
|
|
363
|
+
...UserFields
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
"""
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Linear-specific types and enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
from mcp_ticketer.core.models import Priority, TicketState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LinearPriorityMapping:
|
|
12
|
+
"""Mapping between universal Priority and Linear priority values."""
|
|
13
|
+
|
|
14
|
+
# Linear uses numeric priorities: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
15
|
+
TO_LINEAR: Dict[Priority, int] = {
|
|
16
|
+
Priority.CRITICAL: 1, # Urgent
|
|
17
|
+
Priority.HIGH: 2, # High
|
|
18
|
+
Priority.MEDIUM: 3, # Medium
|
|
19
|
+
Priority.LOW: 4, # Low
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
FROM_LINEAR: Dict[int, Priority] = {
|
|
23
|
+
0: Priority.LOW, # No priority -> Low
|
|
24
|
+
1: Priority.CRITICAL, # Urgent -> Critical
|
|
25
|
+
2: Priority.HIGH, # High -> High
|
|
26
|
+
3: Priority.MEDIUM, # Medium -> Medium
|
|
27
|
+
4: Priority.LOW, # Low -> Low
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LinearStateMapping:
|
|
32
|
+
"""Mapping between universal TicketState and Linear workflow state types."""
|
|
33
|
+
|
|
34
|
+
# Linear workflow state types
|
|
35
|
+
TO_LINEAR: Dict[TicketState, str] = {
|
|
36
|
+
TicketState.OPEN: "unstarted",
|
|
37
|
+
TicketState.IN_PROGRESS: "started",
|
|
38
|
+
TicketState.READY: "unstarted", # No direct equivalent, use unstarted
|
|
39
|
+
TicketState.TESTED: "started", # No direct equivalent, use started
|
|
40
|
+
TicketState.DONE: "completed",
|
|
41
|
+
TicketState.CLOSED: "canceled",
|
|
42
|
+
TicketState.WAITING: "unstarted",
|
|
43
|
+
TicketState.BLOCKED: "unstarted",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
FROM_LINEAR: Dict[str, TicketState] = {
|
|
47
|
+
"backlog": TicketState.OPEN,
|
|
48
|
+
"unstarted": TicketState.OPEN,
|
|
49
|
+
"started": TicketState.IN_PROGRESS,
|
|
50
|
+
"completed": TicketState.DONE,
|
|
51
|
+
"canceled": TicketState.CLOSED,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LinearWorkflowStateType(Enum):
|
|
56
|
+
"""Linear workflow state types."""
|
|
57
|
+
|
|
58
|
+
BACKLOG = "backlog"
|
|
59
|
+
UNSTARTED = "unstarted"
|
|
60
|
+
STARTED = "started"
|
|
61
|
+
COMPLETED = "completed"
|
|
62
|
+
CANCELED = "canceled"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LinearProjectState(Enum):
|
|
66
|
+
"""Linear project states."""
|
|
67
|
+
|
|
68
|
+
PLANNED = "planned"
|
|
69
|
+
STARTED = "started"
|
|
70
|
+
COMPLETED = "completed"
|
|
71
|
+
CANCELED = "canceled"
|
|
72
|
+
PAUSED = "paused"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LinearIssueRelationType(Enum):
|
|
76
|
+
"""Linear issue relation types."""
|
|
77
|
+
|
|
78
|
+
BLOCKS = "blocks"
|
|
79
|
+
BLOCKED_BY = "blockedBy"
|
|
80
|
+
DUPLICATE = "duplicate"
|
|
81
|
+
DUPLICATED_BY = "duplicatedBy"
|
|
82
|
+
RELATES = "relates"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class LinearCommentType(Enum):
|
|
86
|
+
"""Linear comment types."""
|
|
87
|
+
|
|
88
|
+
COMMENT = "comment"
|
|
89
|
+
SYSTEM = "system"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_linear_priority(priority: Priority) -> int:
|
|
93
|
+
"""Convert universal Priority to Linear priority value.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
priority: Universal priority enum
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Linear priority integer (0-4)
|
|
100
|
+
"""
|
|
101
|
+
return LinearPriorityMapping.TO_LINEAR.get(priority, 3) # Default to Medium
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_universal_priority(linear_priority: int) -> Priority:
|
|
105
|
+
"""Convert Linear priority value to universal Priority.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
linear_priority: Linear priority integer (0-4)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Universal priority enum
|
|
112
|
+
"""
|
|
113
|
+
return LinearPriorityMapping.FROM_LINEAR.get(linear_priority, Priority.MEDIUM)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_linear_state_type(state: TicketState) -> str:
|
|
117
|
+
"""Convert universal TicketState to Linear workflow state type.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
state: Universal ticket state enum
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Linear workflow state type string
|
|
124
|
+
"""
|
|
125
|
+
return LinearStateMapping.TO_LINEAR.get(state, "unstarted")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_universal_state(linear_state_type: str) -> TicketState:
|
|
129
|
+
"""Convert Linear workflow state type to universal TicketState.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
linear_state_type: Linear workflow state type string
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Universal ticket state enum
|
|
136
|
+
"""
|
|
137
|
+
return LinearStateMapping.FROM_LINEAR.get(linear_state_type, TicketState.OPEN)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def build_issue_filter(
|
|
141
|
+
state: TicketState | None = None,
|
|
142
|
+
assignee_id: str | None = None,
|
|
143
|
+
priority: Priority | None = None,
|
|
144
|
+
team_id: str | None = None,
|
|
145
|
+
project_id: str | None = None,
|
|
146
|
+
labels: list[str] | None = None,
|
|
147
|
+
created_after: str | None = None,
|
|
148
|
+
updated_after: str | None = None,
|
|
149
|
+
due_before: str | None = None,
|
|
150
|
+
include_archived: bool = False,
|
|
151
|
+
) -> Dict[str, Any]:
|
|
152
|
+
"""Build a Linear issue filter from parameters.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
state: Filter by ticket state
|
|
156
|
+
assignee_id: Filter by assignee Linear user ID
|
|
157
|
+
priority: Filter by priority
|
|
158
|
+
team_id: Filter by team ID
|
|
159
|
+
project_id: Filter by project ID
|
|
160
|
+
labels: Filter by label names
|
|
161
|
+
created_after: Filter by creation date (ISO string)
|
|
162
|
+
updated_after: Filter by update date (ISO string)
|
|
163
|
+
due_before: Filter by due date (ISO string)
|
|
164
|
+
include_archived: Whether to include archived issues
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Linear GraphQL filter object
|
|
168
|
+
"""
|
|
169
|
+
issue_filter: Dict[str, Any] = {}
|
|
170
|
+
|
|
171
|
+
# Team filter (required for most operations)
|
|
172
|
+
if team_id:
|
|
173
|
+
issue_filter["team"] = {"id": {"eq": team_id}}
|
|
174
|
+
|
|
175
|
+
# State filter
|
|
176
|
+
if state:
|
|
177
|
+
state_type = get_linear_state_type(state)
|
|
178
|
+
issue_filter["state"] = {"type": {"eq": state_type}}
|
|
179
|
+
|
|
180
|
+
# Assignee filter
|
|
181
|
+
if assignee_id:
|
|
182
|
+
issue_filter["assignee"] = {"id": {"eq": assignee_id}}
|
|
183
|
+
|
|
184
|
+
# Priority filter
|
|
185
|
+
if priority:
|
|
186
|
+
linear_priority = get_linear_priority(priority)
|
|
187
|
+
issue_filter["priority"] = {"eq": linear_priority}
|
|
188
|
+
|
|
189
|
+
# Project filter
|
|
190
|
+
if project_id:
|
|
191
|
+
issue_filter["project"] = {"id": {"eq": project_id}}
|
|
192
|
+
|
|
193
|
+
# Labels filter
|
|
194
|
+
if labels:
|
|
195
|
+
issue_filter["labels"] = {"some": {"name": {"in": labels}}}
|
|
196
|
+
|
|
197
|
+
# Date filters
|
|
198
|
+
if created_after:
|
|
199
|
+
issue_filter["createdAt"] = {"gte": created_after}
|
|
200
|
+
if updated_after:
|
|
201
|
+
issue_filter["updatedAt"] = {"gte": updated_after}
|
|
202
|
+
if due_before:
|
|
203
|
+
issue_filter["dueDate"] = {"lte": due_before}
|
|
204
|
+
|
|
205
|
+
# Archived filter
|
|
206
|
+
if not include_archived:
|
|
207
|
+
issue_filter["archivedAt"] = {"null": True}
|
|
208
|
+
|
|
209
|
+
return issue_filter
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def build_project_filter(
|
|
213
|
+
state: str | None = None,
|
|
214
|
+
team_id: str | None = None,
|
|
215
|
+
include_completed: bool = True,
|
|
216
|
+
) -> Dict[str, Any]:
|
|
217
|
+
"""Build a Linear project filter from parameters.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
state: Filter by project state
|
|
221
|
+
team_id: Filter by team ID
|
|
222
|
+
include_completed: Whether to include completed projects
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Linear GraphQL filter object
|
|
226
|
+
"""
|
|
227
|
+
project_filter: Dict[str, Any] = {}
|
|
228
|
+
|
|
229
|
+
# Team filter
|
|
230
|
+
if team_id:
|
|
231
|
+
project_filter["teams"] = {"some": {"id": {"eq": team_id}}}
|
|
232
|
+
|
|
233
|
+
# State filter
|
|
234
|
+
if state:
|
|
235
|
+
project_filter["state"] = {"eq": state}
|
|
236
|
+
elif not include_completed:
|
|
237
|
+
# Exclude completed projects by default
|
|
238
|
+
project_filter["state"] = {"neq": "completed"}
|
|
239
|
+
|
|
240
|
+
return project_filter
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def extract_linear_metadata(issue_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
244
|
+
"""Extract Linear-specific metadata from issue data.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
issue_data: Raw Linear issue data from GraphQL
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Dictionary of Linear-specific metadata
|
|
251
|
+
"""
|
|
252
|
+
metadata = {}
|
|
253
|
+
|
|
254
|
+
# Extract Linear-specific fields
|
|
255
|
+
if "dueDate" in issue_data and issue_data["dueDate"]:
|
|
256
|
+
metadata["due_date"] = issue_data["dueDate"]
|
|
257
|
+
|
|
258
|
+
if "cycle" in issue_data and issue_data["cycle"]:
|
|
259
|
+
metadata["cycle_id"] = issue_data["cycle"]["id"]
|
|
260
|
+
metadata["cycle_name"] = issue_data["cycle"]["name"]
|
|
261
|
+
|
|
262
|
+
if "estimate" in issue_data and issue_data["estimate"]:
|
|
263
|
+
metadata["estimate"] = issue_data["estimate"]
|
|
264
|
+
|
|
265
|
+
if "branchName" in issue_data and issue_data["branchName"]:
|
|
266
|
+
metadata["branch_name"] = issue_data["branchName"]
|
|
267
|
+
|
|
268
|
+
if "url" in issue_data:
|
|
269
|
+
metadata["linear_url"] = issue_data["url"]
|
|
270
|
+
|
|
271
|
+
if "slaBreachesAt" in issue_data and issue_data["slaBreachesAt"]:
|
|
272
|
+
metadata["sla_breaches_at"] = issue_data["slaBreachesAt"]
|
|
273
|
+
|
|
274
|
+
if "customerTicketCount" in issue_data:
|
|
275
|
+
metadata["customer_ticket_count"] = issue_data["customerTicketCount"]
|
|
276
|
+
|
|
277
|
+
return metadata
|