@alasano/pi-linear 0.0.1

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.
@@ -0,0 +1,674 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import {
4
+ withLinearAuth,
5
+ linearGraphQL,
6
+ resolveIssueId,
7
+ resolveTeamId,
8
+ fetchIssueByIdentifier,
9
+ } from '../client';
10
+ import {
11
+ PaginationParams,
12
+ FilterParam,
13
+ SortParam,
14
+ TeamConvenienceParams,
15
+ RawInputParam,
16
+ } from '../params';
17
+ import { ISSUE_SELECTION } from '../selections';
18
+ import type { LinearIssue, JsonObject } from '../types';
19
+ import { compactObject, asObject, asObjectArray, asString, mergeFilters } from '../util';
20
+
21
+ export function issueTools() {
22
+ return [
23
+ defineTool({
24
+ name: 'linear_list_issues',
25
+ label: 'Linear List Issues',
26
+ description:
27
+ 'List Linear issues. Supports full issues query args (after, before, filter, first, includeArchived, last, orderBy, sort) and convenience filters (query, teamKey, teamId, stateName, assigneeId).',
28
+ parameters: Type.Object({
29
+ query: Type.Optional(
30
+ Type.String({
31
+ description: 'Convenience filter: title contains this text (containsIgnoreCase).',
32
+ }),
33
+ ),
34
+ stateName: Type.Optional(
35
+ Type.String({
36
+ description: 'Convenience filter: state name equals this value.',
37
+ }),
38
+ ),
39
+ assigneeId: Type.Optional(
40
+ Type.String({
41
+ description: 'Convenience filter: assignee id equals this value.',
42
+ }),
43
+ ),
44
+ ...TeamConvenienceParams,
45
+ ...PaginationParams,
46
+ ...FilterParam,
47
+ ...SortParam,
48
+ }),
49
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
50
+ return withLinearAuth(ctx, signal, async (apiKey) => {
51
+ const convenienceFilter = compactObject({
52
+ title: params.query ? { containsIgnoreCase: params.query } : undefined,
53
+ team: params.teamKey
54
+ ? { key: { eq: params.teamKey } }
55
+ : params.teamId
56
+ ? { id: { eq: params.teamId } }
57
+ : undefined,
58
+ state: params.stateName ? { name: { eq: params.stateName } } : undefined,
59
+ assignee: params.assigneeId ? { id: { eq: params.assigneeId } } : undefined,
60
+ }) as JsonObject;
61
+
62
+ const filter = mergeFilters(
63
+ asObject(params.filter),
64
+ Object.keys(convenienceFilter).length ? convenienceFilter : undefined,
65
+ );
66
+
67
+ const variables = compactObject({
68
+ after: params.after,
69
+ before: params.before,
70
+ filter,
71
+ first: params.first ?? 20,
72
+ includeArchived: params.includeArchived,
73
+ last: params.last,
74
+ orderBy: params.orderBy,
75
+ sort: asObjectArray(params.sort),
76
+ });
77
+
78
+ const data = await linearGraphQL<{ issues: { nodes: LinearIssue[] } }>(
79
+ apiKey,
80
+ `query ListIssues(
81
+ $after: String
82
+ $before: String
83
+ $filter: IssueFilter
84
+ $first: Int
85
+ $includeArchived: Boolean
86
+ $last: Int
87
+ $orderBy: PaginationOrderBy
88
+ $sort: [IssueSortInput!]
89
+ ) {
90
+ issues(
91
+ after: $after
92
+ before: $before
93
+ filter: $filter
94
+ first: $first
95
+ includeArchived: $includeArchived
96
+ last: $last
97
+ orderBy: $orderBy
98
+ sort: $sort
99
+ ) {
100
+ nodes {
101
+ ${ISSUE_SELECTION}
102
+ }
103
+ }
104
+ }`,
105
+ variables,
106
+ signal,
107
+ );
108
+
109
+ const issues = data.issues.nodes;
110
+ return {
111
+ content: [{ type: 'text', text: JSON.stringify({ issues }, null, 2) }],
112
+ details: { issues },
113
+ };
114
+ });
115
+ },
116
+ }),
117
+ defineTool({
118
+ name: 'linear_get_issue',
119
+ label: 'Linear Get Issue',
120
+ description: 'Get full details for a Linear issue by identifier (e.g. ENG-123) or issue id.',
121
+ parameters: Type.Object({
122
+ issue: Type.String({
123
+ description: 'Issue identifier (ENG-123) or issue id.',
124
+ }),
125
+ }),
126
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
127
+ return withLinearAuth(ctx, signal, async (apiKey) => {
128
+ const issueRef = params.issue.trim();
129
+ const identifierIssue = await fetchIssueByIdentifier(apiKey, issueRef, signal);
130
+
131
+ const issue =
132
+ identifierIssue ||
133
+ (
134
+ await linearGraphQL<{ issue: LinearIssue | null }>(
135
+ apiKey,
136
+ `query GetIssueById($id: String!) {
137
+ issue(id: $id) {
138
+ ${ISSUE_SELECTION}
139
+ }
140
+ }`,
141
+ { id: issueRef },
142
+ signal,
143
+ )
144
+ ).issue;
145
+
146
+ return {
147
+ content: [{ type: 'text', text: JSON.stringify({ issue: issue ?? null }, null, 2) }],
148
+ details: { issue: issue ?? null },
149
+ };
150
+ });
151
+ },
152
+ }),
153
+ defineTool({
154
+ name: 'linear_create_issue',
155
+ label: 'Linear Create Issue',
156
+ description:
157
+ 'Create a Linear issue. Supports all IssueCreateInput fields via top-level params and/or input object. Provide teamId or teamKey (or teamId inside input).',
158
+ parameters: Type.Object({
159
+ ...TeamConvenienceParams,
160
+ title: Type.Optional(Type.String({ description: 'Issue title.' })),
161
+ description: Type.Optional(Type.String({ description: 'Issue description in markdown.' })),
162
+ assigneeId: Type.Optional(Type.String({ description: 'IssueCreateInput.assigneeId' })),
163
+ completedAt: Type.Optional(Type.String({ description: 'IssueCreateInput.completedAt' })),
164
+ createAsUser: Type.Optional(Type.String({ description: 'IssueCreateInput.createAsUser' })),
165
+ createdAt: Type.Optional(Type.String({ description: 'IssueCreateInput.createdAt' })),
166
+ cycleId: Type.Optional(Type.String({ description: 'IssueCreateInput.cycleId' })),
167
+ delegateId: Type.Optional(Type.String({ description: 'IssueCreateInput.delegateId' })),
168
+ descriptionData: Type.Optional(Type.Record(Type.String(), Type.Any())),
169
+ displayIconUrl: Type.Optional(
170
+ Type.String({ description: 'IssueCreateInput.displayIconUrl' }),
171
+ ),
172
+ dueDate: Type.Optional(
173
+ Type.String({ description: 'IssueCreateInput.dueDate (YYYY-MM-DD)' }),
174
+ ),
175
+ estimate: Type.Optional(Type.Integer({ description: 'IssueCreateInput.estimate' })),
176
+ id: Type.Optional(Type.String({ description: 'IssueCreateInput.id' })),
177
+ labelIds: Type.Optional(
178
+ Type.Array(Type.String(), { description: 'IssueCreateInput.labelIds' }),
179
+ ),
180
+ lastAppliedTemplateId: Type.Optional(
181
+ Type.String({ description: 'IssueCreateInput.lastAppliedTemplateId' }),
182
+ ),
183
+ parentId: Type.Optional(Type.String({ description: 'IssueCreateInput.parentId' })),
184
+ preserveSortOrderOnCreate: Type.Optional(
185
+ Type.Boolean({
186
+ description: 'IssueCreateInput.preserveSortOrderOnCreate',
187
+ }),
188
+ ),
189
+ priority: Type.Optional(
190
+ Type.Number({
191
+ minimum: 0,
192
+ maximum: 4,
193
+ description: 'IssueCreateInput.priority (0 none, 1 urgent, 2 high, 3 normal, 4 low).',
194
+ }),
195
+ ),
196
+ prioritySortOrder: Type.Optional(
197
+ Type.Number({ description: 'IssueCreateInput.prioritySortOrder' }),
198
+ ),
199
+ projectId: Type.Optional(Type.String({ description: 'IssueCreateInput.projectId' })),
200
+ projectMilestoneId: Type.Optional(
201
+ Type.String({ description: 'IssueCreateInput.projectMilestoneId' }),
202
+ ),
203
+ referenceCommentId: Type.Optional(
204
+ Type.String({ description: 'IssueCreateInput.referenceCommentId' }),
205
+ ),
206
+ slaBreachesAt: Type.Optional(
207
+ Type.String({ description: 'IssueCreateInput.slaBreachesAt' }),
208
+ ),
209
+ slaStartedAt: Type.Optional(Type.String({ description: 'IssueCreateInput.slaStartedAt' })),
210
+ slaType: Type.Optional(Type.String({ description: 'IssueCreateInput.slaType' })),
211
+ sortOrder: Type.Optional(Type.Number({ description: 'IssueCreateInput.sortOrder' })),
212
+ sourceCommentId: Type.Optional(
213
+ Type.String({ description: 'IssueCreateInput.sourceCommentId' }),
214
+ ),
215
+ sourcePullRequestCommentId: Type.Optional(
216
+ Type.String({
217
+ description: 'IssueCreateInput.sourcePullRequestCommentId',
218
+ }),
219
+ ),
220
+ stateId: Type.Optional(Type.String({ description: 'IssueCreateInput.stateId' })),
221
+ subIssueSortOrder: Type.Optional(
222
+ Type.Number({ description: 'IssueCreateInput.subIssueSortOrder' }),
223
+ ),
224
+ subscriberIds: Type.Optional(
225
+ Type.Array(Type.String(), {
226
+ description: 'IssueCreateInput.subscriberIds',
227
+ }),
228
+ ),
229
+ templateId: Type.Optional(Type.String({ description: 'IssueCreateInput.templateId' })),
230
+ useDefaultTemplate: Type.Optional(
231
+ Type.Boolean({ description: 'IssueCreateInput.useDefaultTemplate' }),
232
+ ),
233
+ ...RawInputParam,
234
+ }),
235
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
236
+ return withLinearAuth(ctx, signal, async (apiKey) => {
237
+ const rawInput = asObject(params.input) || {};
238
+ const rawInputTeamId = asString(rawInput.teamId);
239
+
240
+ const teamId = await resolveTeamId(
241
+ apiKey,
242
+ {
243
+ teamId: params.teamId || rawInputTeamId,
244
+ teamKey: params.teamKey,
245
+ },
246
+ signal,
247
+ );
248
+
249
+ const convenienceInput = compactObject({
250
+ assigneeId: params.assigneeId,
251
+ completedAt: params.completedAt,
252
+ createAsUser: params.createAsUser,
253
+ createdAt: params.createdAt,
254
+ cycleId: params.cycleId,
255
+ delegateId: params.delegateId,
256
+ description: params.description,
257
+ descriptionData: asObject(params.descriptionData),
258
+ displayIconUrl: params.displayIconUrl,
259
+ dueDate: params.dueDate,
260
+ estimate: params.estimate,
261
+ id: params.id,
262
+ labelIds: params.labelIds,
263
+ lastAppliedTemplateId: params.lastAppliedTemplateId,
264
+ parentId: params.parentId,
265
+ preserveSortOrderOnCreate: params.preserveSortOrderOnCreate,
266
+ priority: params.priority,
267
+ prioritySortOrder: params.prioritySortOrder,
268
+ projectId: params.projectId,
269
+ projectMilestoneId: params.projectMilestoneId,
270
+ referenceCommentId: params.referenceCommentId,
271
+ slaBreachesAt: params.slaBreachesAt,
272
+ slaStartedAt: params.slaStartedAt,
273
+ slaType: params.slaType,
274
+ sortOrder: params.sortOrder,
275
+ sourceCommentId: params.sourceCommentId,
276
+ sourcePullRequestCommentId: params.sourcePullRequestCommentId,
277
+ stateId: params.stateId,
278
+ subIssueSortOrder: params.subIssueSortOrder,
279
+ subscriberIds: params.subscriberIds,
280
+ teamId,
281
+ templateId: params.templateId,
282
+ title: params.title,
283
+ useDefaultTemplate: params.useDefaultTemplate,
284
+ });
285
+
286
+ const input = {
287
+ ...rawInput,
288
+ ...convenienceInput,
289
+ teamId,
290
+ };
291
+
292
+ if (!asString(input.title)) {
293
+ throw new Error('Issue title is required for issueCreate (title).');
294
+ }
295
+
296
+ const data = await linearGraphQL<{
297
+ issueCreate: { success: boolean; issue?: LinearIssue | null };
298
+ }>(
299
+ apiKey,
300
+ `mutation CreateIssue($input: IssueCreateInput!) {
301
+ issueCreate(input: $input) {
302
+ success
303
+ issue {
304
+ ${ISSUE_SELECTION}
305
+ }
306
+ }
307
+ }`,
308
+ { input },
309
+ signal,
310
+ );
311
+
312
+ if (!data.issueCreate.success || !data.issueCreate.issue) {
313
+ throw new Error('Linear issueCreate did not succeed.');
314
+ }
315
+
316
+ const issue = data.issueCreate.issue;
317
+ return {
318
+ content: [{ type: 'text', text: JSON.stringify({ issue }, null, 2) }],
319
+ details: { issue },
320
+ };
321
+ });
322
+ },
323
+ }),
324
+ defineTool({
325
+ name: 'linear_update_issue',
326
+ label: 'Linear Update Issue',
327
+ description:
328
+ 'Update a Linear issue by identifier (ENG-123) or issue id. Supports all IssueUpdateInput fields via top-level params and/or input object. Use clearDueDate=true (or dueDate=null in input) to clear due date.',
329
+ parameters: Type.Object({
330
+ issue: Type.String({
331
+ description: 'Issue identifier (ENG-123) or issue id.',
332
+ }),
333
+ title: Type.Optional(Type.String({ description: 'IssueUpdateInput.title' })),
334
+ description: Type.Optional(Type.String({ description: 'IssueUpdateInput.description' })),
335
+ priority: Type.Optional(
336
+ Type.Number({
337
+ minimum: 0,
338
+ maximum: 4,
339
+ description: 'IssueUpdateInput.priority (0 none, 1 urgent, 2 high, 3 normal, 4 low).',
340
+ }),
341
+ ),
342
+ stateId: Type.Optional(Type.String({ description: 'IssueUpdateInput.stateId' })),
343
+ assigneeId: Type.Optional(Type.String({ description: 'IssueUpdateInput.assigneeId' })),
344
+ dueDate: Type.Optional(
345
+ Type.String({
346
+ description: 'IssueUpdateInput.dueDate (YYYY-MM-DD). Empty string clears.',
347
+ }),
348
+ ),
349
+ clearDueDate: Type.Optional(
350
+ Type.Boolean({ description: 'If true, dueDate is set to null.' }),
351
+ ),
352
+ addedLabelIds: Type.Optional(
353
+ Type.Array(Type.String(), {
354
+ description: 'IssueUpdateInput.addedLabelIds',
355
+ }),
356
+ ),
357
+ autoClosedByParentClosing: Type.Optional(
358
+ Type.Boolean({
359
+ description: 'IssueUpdateInput.autoClosedByParentClosing',
360
+ }),
361
+ ),
362
+ cycleId: Type.Optional(Type.String({ description: 'IssueUpdateInput.cycleId' })),
363
+ delegateId: Type.Optional(Type.String({ description: 'IssueUpdateInput.delegateId' })),
364
+ descriptionData: Type.Optional(Type.Record(Type.String(), Type.Any())),
365
+ estimate: Type.Optional(Type.Integer({ description: 'IssueUpdateInput.estimate' })),
366
+ labelIds: Type.Optional(
367
+ Type.Array(Type.String(), { description: 'IssueUpdateInput.labelIds' }),
368
+ ),
369
+ lastAppliedTemplateId: Type.Optional(
370
+ Type.String({ description: 'IssueUpdateInput.lastAppliedTemplateId' }),
371
+ ),
372
+ parentId: Type.Optional(Type.String({ description: 'IssueUpdateInput.parentId' })),
373
+ prioritySortOrder: Type.Optional(
374
+ Type.Number({ description: 'IssueUpdateInput.prioritySortOrder' }),
375
+ ),
376
+ projectId: Type.Optional(Type.String({ description: 'IssueUpdateInput.projectId' })),
377
+ projectMilestoneId: Type.Optional(
378
+ Type.String({ description: 'IssueUpdateInput.projectMilestoneId' }),
379
+ ),
380
+ removedLabelIds: Type.Optional(
381
+ Type.Array(Type.String(), {
382
+ description: 'IssueUpdateInput.removedLabelIds',
383
+ }),
384
+ ),
385
+ slaBreachesAt: Type.Optional(
386
+ Type.String({ description: 'IssueUpdateInput.slaBreachesAt' }),
387
+ ),
388
+ slaStartedAt: Type.Optional(Type.String({ description: 'IssueUpdateInput.slaStartedAt' })),
389
+ slaType: Type.Optional(Type.String({ description: 'IssueUpdateInput.slaType' })),
390
+ snoozedById: Type.Optional(Type.String({ description: 'IssueUpdateInput.snoozedById' })),
391
+ snoozedUntilAt: Type.Optional(
392
+ Type.String({ description: 'IssueUpdateInput.snoozedUntilAt' }),
393
+ ),
394
+ sortOrder: Type.Optional(Type.Number({ description: 'IssueUpdateInput.sortOrder' })),
395
+ subIssueSortOrder: Type.Optional(
396
+ Type.Number({ description: 'IssueUpdateInput.subIssueSortOrder' }),
397
+ ),
398
+ subscriberIds: Type.Optional(
399
+ Type.Array(Type.String(), {
400
+ description: 'IssueUpdateInput.subscriberIds',
401
+ }),
402
+ ),
403
+ teamId: Type.Optional(Type.String({ description: 'IssueUpdateInput.teamId' })),
404
+ trashed: Type.Optional(Type.Boolean({ description: 'IssueUpdateInput.trashed' })),
405
+ ...RawInputParam,
406
+ }),
407
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
408
+ return withLinearAuth(ctx, signal, async (apiKey) => {
409
+ const issueId = await resolveIssueId(apiKey, params.issue, signal);
410
+ const rawInput = asObject(params.input) || {};
411
+
412
+ const dueDate =
413
+ params.clearDueDate || params.dueDate === ''
414
+ ? null
415
+ : params.dueDate !== undefined
416
+ ? params.dueDate
417
+ : undefined;
418
+
419
+ const convenienceInput = compactObject({
420
+ addedLabelIds: params.addedLabelIds,
421
+ assigneeId: params.assigneeId,
422
+ autoClosedByParentClosing: params.autoClosedByParentClosing,
423
+ cycleId: params.cycleId,
424
+ delegateId: params.delegateId,
425
+ description: params.description,
426
+ descriptionData: asObject(params.descriptionData),
427
+ dueDate,
428
+ estimate: params.estimate,
429
+ labelIds: params.labelIds,
430
+ lastAppliedTemplateId: params.lastAppliedTemplateId,
431
+ parentId: params.parentId,
432
+ priority: params.priority,
433
+ prioritySortOrder: params.prioritySortOrder,
434
+ projectId: params.projectId,
435
+ projectMilestoneId: params.projectMilestoneId,
436
+ removedLabelIds: params.removedLabelIds,
437
+ slaBreachesAt: params.slaBreachesAt,
438
+ slaStartedAt: params.slaStartedAt,
439
+ slaType: params.slaType,
440
+ snoozedById: params.snoozedById,
441
+ snoozedUntilAt: params.snoozedUntilAt,
442
+ sortOrder: params.sortOrder,
443
+ stateId: params.stateId,
444
+ subIssueSortOrder: params.subIssueSortOrder,
445
+ subscriberIds: params.subscriberIds,
446
+ teamId: params.teamId,
447
+ title: params.title,
448
+ trashed: params.trashed,
449
+ });
450
+
451
+ const input = {
452
+ ...rawInput,
453
+ ...convenienceInput,
454
+ };
455
+
456
+ if (Object.keys(input).length === 0) {
457
+ throw new Error('No update fields were provided.');
458
+ }
459
+
460
+ const data = await linearGraphQL<{
461
+ issueUpdate: { success: boolean; issue?: LinearIssue | null };
462
+ }>(
463
+ apiKey,
464
+ `mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
465
+ issueUpdate(id: $id, input: $input) {
466
+ success
467
+ issue {
468
+ ${ISSUE_SELECTION}
469
+ }
470
+ }
471
+ }`,
472
+ { id: issueId, input },
473
+ signal,
474
+ );
475
+
476
+ if (!data.issueUpdate.success || !data.issueUpdate.issue) {
477
+ throw new Error('Linear issueUpdate did not succeed.');
478
+ }
479
+
480
+ const issue = data.issueUpdate.issue;
481
+ return {
482
+ content: [{ type: 'text', text: JSON.stringify({ issue }, null, 2) }],
483
+ details: { issue },
484
+ };
485
+ });
486
+ },
487
+ }),
488
+ defineTool({
489
+ name: 'linear_delete_issue',
490
+ label: 'Linear Delete Issue',
491
+ description: 'Delete an issue by identifier (ENG-123) or id. Admins can permanently delete.',
492
+ parameters: Type.Object({
493
+ issue: Type.String({
494
+ description: 'Issue identifier (ENG-123) or issue id.',
495
+ }),
496
+ permanentlyDelete: Type.Optional(Type.Boolean()),
497
+ }),
498
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
499
+ return withLinearAuth(ctx, signal, async (apiKey) => {
500
+ const issueId = await resolveIssueId(apiKey, params.issue, signal);
501
+
502
+ const data = await linearGraphQL<{
503
+ issueDelete: { success: boolean };
504
+ }>(
505
+ apiKey,
506
+ `mutation DeleteIssue($id: String!, $permanentlyDelete: Boolean) {
507
+ issueDelete(id: $id, permanentlyDelete: $permanentlyDelete) {
508
+ success
509
+ }
510
+ }`,
511
+ { id: issueId, permanentlyDelete: params.permanentlyDelete },
512
+ signal,
513
+ );
514
+
515
+ if (!data.issueDelete.success) {
516
+ throw new Error('Linear issueDelete did not succeed.');
517
+ }
518
+
519
+ return {
520
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
521
+ details: { success: true },
522
+ };
523
+ });
524
+ },
525
+ }),
526
+ defineTool({
527
+ name: 'linear_archive_issue',
528
+ label: 'Linear Archive Issue',
529
+ description:
530
+ 'Archive an issue by identifier (ENG-123) or id. Use trash=true to trash instead.',
531
+ parameters: Type.Object({
532
+ issue: Type.String({
533
+ description: 'Issue identifier (ENG-123) or issue id.',
534
+ }),
535
+ trash: Type.Optional(Type.Boolean()),
536
+ }),
537
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
538
+ return withLinearAuth(ctx, signal, async (apiKey) => {
539
+ const issueId = await resolveIssueId(apiKey, params.issue, signal);
540
+
541
+ const data = await linearGraphQL<{
542
+ issueArchive: { success: boolean };
543
+ }>(
544
+ apiKey,
545
+ `mutation ArchiveIssue($id: String!, $trash: Boolean) {
546
+ issueArchive(id: $id, trash: $trash) {
547
+ success
548
+ }
549
+ }`,
550
+ { id: issueId, trash: params.trash },
551
+ signal,
552
+ );
553
+
554
+ if (!data.issueArchive.success) {
555
+ throw new Error('Linear issueArchive did not succeed.');
556
+ }
557
+
558
+ return {
559
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
560
+ details: { success: true },
561
+ };
562
+ });
563
+ },
564
+ }),
565
+ defineTool({
566
+ name: 'linear_unarchive_issue',
567
+ label: 'Linear Unarchive Issue',
568
+ description: 'Unarchive an issue by identifier (ENG-123) or id.',
569
+ parameters: Type.Object({
570
+ issue: Type.String({
571
+ description: 'Issue identifier (ENG-123) or issue id.',
572
+ }),
573
+ }),
574
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
575
+ return withLinearAuth(ctx, signal, async (apiKey) => {
576
+ const issueId = await resolveIssueId(apiKey, params.issue, signal);
577
+
578
+ const data = await linearGraphQL<{
579
+ issueUnarchive: { success: boolean };
580
+ }>(
581
+ apiKey,
582
+ `mutation UnarchiveIssue($id: String!) {
583
+ issueUnarchive(id: $id) {
584
+ success
585
+ }
586
+ }`,
587
+ { id: issueId },
588
+ signal,
589
+ );
590
+
591
+ if (!data.issueUnarchive.success) {
592
+ throw new Error('Linear issueUnarchive did not succeed.');
593
+ }
594
+
595
+ return {
596
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
597
+ details: { success: true },
598
+ };
599
+ });
600
+ },
601
+ }),
602
+ defineTool({
603
+ name: 'linear_search_issues',
604
+ label: 'Linear Search Issues',
605
+ description: 'Search issues by text. Supports searching in comments and boosting by team.',
606
+ parameters: Type.Object({
607
+ term: Type.String({ description: 'Search text.' }),
608
+ includeComments: Type.Optional(Type.Boolean({ description: 'Search in comments too.' })),
609
+ teamId: Type.Optional(Type.String({ description: 'Team UUID to boost results for.' })),
610
+ ...PaginationParams,
611
+ ...FilterParam,
612
+ }),
613
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
614
+ return withLinearAuth(ctx, signal, async (apiKey) => {
615
+ const variables = compactObject({
616
+ term: params.term,
617
+ includeComments: params.includeComments,
618
+ teamId: params.teamId,
619
+ after: params.after,
620
+ before: params.before,
621
+ filter: asObject(params.filter),
622
+ first: params.first ?? 20,
623
+ includeArchived: params.includeArchived,
624
+ last: params.last,
625
+ orderBy: params.orderBy,
626
+ });
627
+
628
+ const data = await linearGraphQL<{
629
+ searchIssues: { nodes: LinearIssue[] };
630
+ }>(
631
+ apiKey,
632
+ `query SearchIssues(
633
+ $term: String!
634
+ $includeComments: Boolean
635
+ $teamId: String
636
+ $after: String
637
+ $before: String
638
+ $filter: IssueFilter
639
+ $first: Int
640
+ $includeArchived: Boolean
641
+ $last: Int
642
+ $orderBy: PaginationOrderBy
643
+ ) {
644
+ searchIssues(
645
+ term: $term
646
+ includeComments: $includeComments
647
+ teamId: $teamId
648
+ after: $after
649
+ before: $before
650
+ filter: $filter
651
+ first: $first
652
+ includeArchived: $includeArchived
653
+ last: $last
654
+ orderBy: $orderBy
655
+ ) {
656
+ nodes {
657
+ ${ISSUE_SELECTION}
658
+ }
659
+ }
660
+ }`,
661
+ variables,
662
+ signal,
663
+ );
664
+
665
+ const issues = data.searchIssues.nodes;
666
+ return {
667
+ content: [{ type: 'text', text: JSON.stringify({ issues }, null, 2) }],
668
+ details: { issues },
669
+ };
670
+ });
671
+ },
672
+ }),
673
+ ];
674
+ }