@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,273 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { withLinearAuth, linearGraphQL, resolveTeamId } from '../client';
4
+ import { PaginationParams, FilterParam, RawInputParam, TeamConvenienceParams } from '../params';
5
+ import { ISSUE_LABEL_SELECTION } from '../selections';
6
+ import type { JsonObject } from '../types';
7
+ import { compactObject, asObject, asString, mergeFilters } from '../util';
8
+
9
+ export function issueLabelTools() {
10
+ return [
11
+ defineTool({
12
+ name: 'linear_list_issue_labels',
13
+ label: 'Linear List Issue Labels',
14
+ description: 'List issue labels. Supports full issueLabels query args.',
15
+ parameters: Type.Object({
16
+ ...TeamConvenienceParams,
17
+ ...PaginationParams,
18
+ ...FilterParam,
19
+ }),
20
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
21
+ return withLinearAuth(ctx, signal, async (apiKey) => {
22
+ const resolvedTeamId =
23
+ params.teamId || params.teamKey
24
+ ? await resolveTeamId(
25
+ apiKey,
26
+ { teamId: params.teamId, teamKey: params.teamKey },
27
+ signal,
28
+ )
29
+ : undefined;
30
+
31
+ const convenienceFilter = resolvedTeamId
32
+ ? ({ team: { id: { eq: resolvedTeamId } } } as JsonObject)
33
+ : undefined;
34
+
35
+ const filter = mergeFilters(asObject(params.filter), convenienceFilter);
36
+
37
+ const variables = compactObject({
38
+ after: params.after,
39
+ before: params.before,
40
+ filter,
41
+ first: params.first ?? 50,
42
+ includeArchived: params.includeArchived,
43
+ last: params.last,
44
+ orderBy: params.orderBy,
45
+ });
46
+
47
+ const data = await linearGraphQL<{
48
+ issueLabels: { nodes: Array<JsonObject> };
49
+ }>(
50
+ apiKey,
51
+ `query ListIssueLabels(
52
+ $after: String
53
+ $before: String
54
+ $filter: IssueLabelFilter
55
+ $first: Int
56
+ $includeArchived: Boolean
57
+ $last: Int
58
+ $orderBy: PaginationOrderBy
59
+ ) {
60
+ issueLabels(
61
+ after: $after
62
+ before: $before
63
+ filter: $filter
64
+ first: $first
65
+ includeArchived: $includeArchived
66
+ last: $last
67
+ orderBy: $orderBy
68
+ ) {
69
+ nodes {
70
+ ${ISSUE_LABEL_SELECTION}
71
+ }
72
+ }
73
+ }`,
74
+ variables,
75
+ signal,
76
+ );
77
+
78
+ const labels = data.issueLabels.nodes;
79
+ return {
80
+ content: [{ type: 'text', text: JSON.stringify({ labels }, null, 2) }],
81
+ details: { labels },
82
+ };
83
+ });
84
+ },
85
+ }),
86
+ defineTool({
87
+ name: 'linear_create_issue_label',
88
+ label: 'Linear Create Issue Label',
89
+ description:
90
+ 'Create an issue label via issueLabelCreate. Supports top-level fields and raw input.',
91
+ parameters: Type.Object({
92
+ name: Type.Optional(Type.String()),
93
+ color: Type.Optional(Type.String()),
94
+ description: Type.Optional(Type.String()),
95
+ id: Type.Optional(Type.String()),
96
+ isGroup: Type.Optional(Type.Boolean()),
97
+ parentId: Type.Optional(Type.String()),
98
+ retiredAt: Type.Optional(Type.String()),
99
+ ...TeamConvenienceParams,
100
+ replaceTeamLabels: Type.Optional(Type.Boolean()),
101
+ ...RawInputParam,
102
+ }),
103
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
104
+ return withLinearAuth(ctx, signal, async (apiKey) => {
105
+ const rawInput = asObject(params.input) || {};
106
+ const teamId =
107
+ params.teamId || params.teamKey || asString(rawInput.teamId)
108
+ ? await resolveTeamId(
109
+ apiKey,
110
+ {
111
+ teamId: params.teamId || asString(rawInput.teamId),
112
+ teamKey: params.teamKey,
113
+ },
114
+ signal,
115
+ )
116
+ : undefined;
117
+
118
+ const input = {
119
+ ...rawInput,
120
+ ...compactObject({
121
+ name: params.name,
122
+ color: params.color,
123
+ description: params.description,
124
+ id: params.id,
125
+ isGroup: params.isGroup,
126
+ parentId: params.parentId,
127
+ retiredAt: params.retiredAt,
128
+ teamId,
129
+ }),
130
+ };
131
+
132
+ if (!asString(input.name)) {
133
+ throw new Error('Issue label name is required (name).');
134
+ }
135
+
136
+ const data = await linearGraphQL<{
137
+ issueLabelCreate: {
138
+ success: boolean;
139
+ issueLabel?: JsonObject | null;
140
+ };
141
+ }>(
142
+ apiKey,
143
+ `mutation CreateIssueLabel($input: IssueLabelCreateInput!, $replaceTeamLabels: Boolean) {
144
+ issueLabelCreate(input: $input, replaceTeamLabels: $replaceTeamLabels) {
145
+ success
146
+ issueLabel {
147
+ ${ISSUE_LABEL_SELECTION}
148
+ }
149
+ }
150
+ }`,
151
+ {
152
+ input,
153
+ replaceTeamLabels: params.replaceTeamLabels,
154
+ },
155
+ signal,
156
+ );
157
+
158
+ const label = data.issueLabelCreate.issueLabel;
159
+ if (!data.issueLabelCreate.success || !label) {
160
+ throw new Error('Linear issueLabelCreate did not succeed.');
161
+ }
162
+
163
+ return {
164
+ content: [{ type: 'text', text: JSON.stringify({ label }, null, 2) }],
165
+ details: { label },
166
+ };
167
+ });
168
+ },
169
+ }),
170
+ defineTool({
171
+ name: 'linear_update_issue_label',
172
+ label: 'Linear Update Issue Label',
173
+ description: 'Update an issue label by id.',
174
+ parameters: Type.Object({
175
+ id: Type.String(),
176
+ name: Type.Optional(Type.String()),
177
+ description: Type.Optional(Type.String()),
178
+ color: Type.Optional(Type.String()),
179
+ parentId: Type.Optional(Type.String()),
180
+ isGroup: Type.Optional(Type.Boolean()),
181
+ retiredAt: Type.Optional(Type.String()),
182
+ replaceTeamLabels: Type.Optional(Type.Boolean()),
183
+ ...RawInputParam,
184
+ }),
185
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
186
+ return withLinearAuth(ctx, signal, async (apiKey) => {
187
+ const rawInput = asObject(params.input) || {};
188
+ const input = {
189
+ ...rawInput,
190
+ ...compactObject({
191
+ name: params.name,
192
+ description: params.description,
193
+ color: params.color,
194
+ parentId: params.parentId,
195
+ isGroup: params.isGroup,
196
+ retiredAt: params.retiredAt,
197
+ }),
198
+ };
199
+
200
+ if (Object.keys(input).length === 0) {
201
+ throw new Error('No update fields were provided.');
202
+ }
203
+
204
+ const data = await linearGraphQL<{
205
+ issueLabelUpdate: {
206
+ success: boolean;
207
+ issueLabel?: JsonObject | null;
208
+ };
209
+ }>(
210
+ apiKey,
211
+ `mutation UpdateIssueLabel($id: String!, $input: IssueLabelUpdateInput!, $replaceTeamLabels: Boolean) {
212
+ issueLabelUpdate(id: $id, input: $input, replaceTeamLabels: $replaceTeamLabels) {
213
+ success
214
+ issueLabel {
215
+ ${ISSUE_LABEL_SELECTION}
216
+ }
217
+ }
218
+ }`,
219
+ {
220
+ id: params.id,
221
+ input,
222
+ replaceTeamLabels: params.replaceTeamLabels,
223
+ },
224
+ signal,
225
+ );
226
+
227
+ const label = data.issueLabelUpdate.issueLabel;
228
+ if (!data.issueLabelUpdate.success || !label) {
229
+ throw new Error('Linear issueLabelUpdate did not succeed.');
230
+ }
231
+
232
+ return {
233
+ content: [{ type: 'text', text: JSON.stringify({ label }, null, 2) }],
234
+ details: { label },
235
+ };
236
+ });
237
+ },
238
+ }),
239
+ defineTool({
240
+ name: 'linear_delete_issue_label',
241
+ label: 'Linear Delete Issue Label',
242
+ description: 'Delete an issue label by id.',
243
+ parameters: Type.Object({
244
+ id: Type.String(),
245
+ }),
246
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
247
+ return withLinearAuth(ctx, signal, async (apiKey) => {
248
+ const data = await linearGraphQL<{
249
+ issueLabelDelete: { success: boolean };
250
+ }>(
251
+ apiKey,
252
+ `mutation DeleteIssueLabel($id: String!) {
253
+ issueLabelDelete(id: $id) {
254
+ success
255
+ }
256
+ }`,
257
+ { id: params.id },
258
+ signal,
259
+ );
260
+
261
+ if (!data.issueLabelDelete.success) {
262
+ throw new Error('Linear issueLabelDelete did not succeed.');
263
+ }
264
+
265
+ return {
266
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
267
+ details: { success: true },
268
+ };
269
+ });
270
+ },
271
+ }),
272
+ ];
273
+ }
@@ -0,0 +1,207 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { withLinearAuth, linearGraphQL } from '../client';
4
+ import { PaginationParams } from '../params';
5
+ import { ISSUE_RELATION_SELECTION } from '../selections';
6
+ import type { JsonObject } from '../types';
7
+ import { compactObject } from '../util';
8
+
9
+ export function issueRelationTools() {
10
+ return [
11
+ defineTool({
12
+ name: 'linear_list_issue_relations',
13
+ label: 'Linear List Issue Relations',
14
+ description: 'List issue relations. Supports pagination.',
15
+ parameters: Type.Object({
16
+ ...PaginationParams,
17
+ }),
18
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
19
+ return withLinearAuth(ctx, signal, async (apiKey) => {
20
+ const variables = compactObject({
21
+ after: params.after,
22
+ before: params.before,
23
+ first: params.first ?? 20,
24
+ includeArchived: params.includeArchived,
25
+ last: params.last,
26
+ orderBy: params.orderBy,
27
+ });
28
+
29
+ const data = await linearGraphQL<{
30
+ issueRelations: { nodes: Array<JsonObject> };
31
+ }>(
32
+ apiKey,
33
+ `query ListIssueRelations(
34
+ $after: String
35
+ $before: String
36
+ $first: Int
37
+ $includeArchived: Boolean
38
+ $last: Int
39
+ $orderBy: PaginationOrderBy
40
+ ) {
41
+ issueRelations(
42
+ after: $after
43
+ before: $before
44
+ first: $first
45
+ includeArchived: $includeArchived
46
+ last: $last
47
+ orderBy: $orderBy
48
+ ) {
49
+ nodes {
50
+ ${ISSUE_RELATION_SELECTION}
51
+ }
52
+ }
53
+ }`,
54
+ variables,
55
+ signal,
56
+ );
57
+
58
+ const issueRelations = data.issueRelations.nodes;
59
+ return {
60
+ content: [{ type: 'text', text: JSON.stringify({ issueRelations }, null, 2) }],
61
+ details: { issueRelations },
62
+ };
63
+ });
64
+ },
65
+ }),
66
+ defineTool({
67
+ name: 'linear_create_issue_relation',
68
+ label: 'Linear Create Issue Relation',
69
+ description: 'Create a relation between two issues.',
70
+ parameters: Type.Object({
71
+ issueId: Type.String({
72
+ description: 'Issue identifier (e.g. ENG-123) or UUID.',
73
+ }),
74
+ relatedIssueId: Type.String({
75
+ description: 'Related issue identifier (e.g. ENG-456) or UUID.',
76
+ }),
77
+ type: Type.String({
78
+ description: 'Relation type: blocks, duplicate, related, or similar.',
79
+ }),
80
+ }),
81
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
82
+ return withLinearAuth(ctx, signal, async (apiKey) => {
83
+ const input = {
84
+ issueId: params.issueId,
85
+ relatedIssueId: params.relatedIssueId,
86
+ type: params.type,
87
+ };
88
+
89
+ const data = await linearGraphQL<{
90
+ issueRelationCreate: {
91
+ success: boolean;
92
+ issueRelation?: JsonObject | null;
93
+ };
94
+ }>(
95
+ apiKey,
96
+ `mutation CreateIssueRelation($input: IssueRelationCreateInput!) {
97
+ issueRelationCreate(input: $input) {
98
+ success
99
+ issueRelation {
100
+ ${ISSUE_RELATION_SELECTION}
101
+ }
102
+ }
103
+ }`,
104
+ { input },
105
+ signal,
106
+ );
107
+
108
+ const issueRelation = data.issueRelationCreate.issueRelation;
109
+ if (!data.issueRelationCreate.success || !issueRelation) {
110
+ throw new Error('Linear issueRelationCreate did not succeed.');
111
+ }
112
+
113
+ return {
114
+ content: [{ type: 'text', text: JSON.stringify({ issueRelation }, null, 2) }],
115
+ details: { issueRelation },
116
+ };
117
+ });
118
+ },
119
+ }),
120
+ defineTool({
121
+ name: 'linear_update_issue_relation',
122
+ label: 'Linear Update Issue Relation',
123
+ description: 'Update an issue relation by id.',
124
+ parameters: Type.Object({
125
+ id: Type.String(),
126
+ type: Type.Optional(Type.String()),
127
+ issueId: Type.Optional(Type.String()),
128
+ relatedIssueId: Type.Optional(Type.String()),
129
+ }),
130
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
131
+ return withLinearAuth(ctx, signal, async (apiKey) => {
132
+ const input = compactObject({
133
+ type: params.type,
134
+ issueId: params.issueId,
135
+ relatedIssueId: params.relatedIssueId,
136
+ });
137
+
138
+ if (Object.keys(input).length === 0) {
139
+ throw new Error('No update fields were provided.');
140
+ }
141
+
142
+ const data = await linearGraphQL<{
143
+ issueRelationUpdate: {
144
+ success: boolean;
145
+ issueRelation?: JsonObject | null;
146
+ };
147
+ }>(
148
+ apiKey,
149
+ `mutation UpdateIssueRelation($id: String!, $input: IssueRelationUpdateInput!) {
150
+ issueRelationUpdate(id: $id, input: $input) {
151
+ success
152
+ issueRelation {
153
+ ${ISSUE_RELATION_SELECTION}
154
+ }
155
+ }
156
+ }`,
157
+ { id: params.id, input },
158
+ signal,
159
+ );
160
+
161
+ const issueRelation = data.issueRelationUpdate.issueRelation;
162
+ if (!data.issueRelationUpdate.success || !issueRelation) {
163
+ throw new Error('Linear issueRelationUpdate did not succeed.');
164
+ }
165
+
166
+ return {
167
+ content: [{ type: 'text', text: JSON.stringify({ issueRelation }, null, 2) }],
168
+ details: { issueRelation },
169
+ };
170
+ });
171
+ },
172
+ }),
173
+ defineTool({
174
+ name: 'linear_delete_issue_relation',
175
+ label: 'Linear Delete Issue Relation',
176
+ description: 'Delete an issue relation by id.',
177
+ parameters: Type.Object({
178
+ id: Type.String(),
179
+ }),
180
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
181
+ return withLinearAuth(ctx, signal, async (apiKey) => {
182
+ const data = await linearGraphQL<{
183
+ issueRelationDelete: { success: boolean };
184
+ }>(
185
+ apiKey,
186
+ `mutation DeleteIssueRelation($id: String!) {
187
+ issueRelationDelete(id: $id) {
188
+ success
189
+ }
190
+ }`,
191
+ { id: params.id },
192
+ signal,
193
+ );
194
+
195
+ if (!data.issueRelationDelete.success) {
196
+ throw new Error('Linear issueRelationDelete did not succeed.');
197
+ }
198
+
199
+ return {
200
+ content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
201
+ details: { success: true },
202
+ };
203
+ });
204
+ },
205
+ }),
206
+ ];
207
+ }
@@ -0,0 +1,72 @@
1
+ import { defineTool } from '@mariozechner/pi-coding-agent';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { withLinearAuth, linearGraphQL } from '../client';
4
+ import { PaginationParams, FilterParam } from '../params';
5
+ import { WORKFLOW_STATE_SELECTION } from '../selections';
6
+ import type { JsonObject } from '../types';
7
+ import { compactObject, asObject } from '../util';
8
+
9
+ export function issueStatusTools() {
10
+ return [
11
+ defineTool({
12
+ name: 'linear_list_issue_statuses',
13
+ label: 'Linear List Issue Statuses',
14
+ description:
15
+ 'List workflow states (issue statuses). Supports full workflowStates query args.',
16
+ parameters: Type.Object({
17
+ ...PaginationParams,
18
+ ...FilterParam,
19
+ }),
20
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
21
+ return withLinearAuth(ctx, signal, async (apiKey) => {
22
+ const variables = compactObject({
23
+ after: params.after,
24
+ before: params.before,
25
+ filter: asObject(params.filter),
26
+ first: params.first ?? 50,
27
+ includeArchived: params.includeArchived,
28
+ last: params.last,
29
+ orderBy: params.orderBy,
30
+ });
31
+
32
+ const data = await linearGraphQL<{
33
+ workflowStates: { nodes: Array<JsonObject> };
34
+ }>(
35
+ apiKey,
36
+ `query ListIssueStatuses(
37
+ $after: String
38
+ $before: String
39
+ $filter: WorkflowStateFilter
40
+ $first: Int
41
+ $includeArchived: Boolean
42
+ $last: Int
43
+ $orderBy: PaginationOrderBy
44
+ ) {
45
+ workflowStates(
46
+ after: $after
47
+ before: $before
48
+ filter: $filter
49
+ first: $first
50
+ includeArchived: $includeArchived
51
+ last: $last
52
+ orderBy: $orderBy
53
+ ) {
54
+ nodes {
55
+ ${WORKFLOW_STATE_SELECTION}
56
+ }
57
+ }
58
+ }`,
59
+ variables,
60
+ signal,
61
+ );
62
+
63
+ const states = data.workflowStates.nodes;
64
+ return {
65
+ content: [{ type: 'text', text: JSON.stringify({ states }, null, 2) }],
66
+ details: { states },
67
+ };
68
+ });
69
+ },
70
+ }),
71
+ ];
72
+ }