@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.
- package/README.md +181 -0
- package/assets/screenshot.png +0 -0
- package/extensions/client.ts +291 -0
- package/extensions/index.ts +214 -0
- package/extensions/params.ts +44 -0
- package/extensions/selections.ts +327 -0
- package/extensions/settings.ts +415 -0
- package/extensions/tools/comments.ts +237 -0
- package/extensions/tools/documents.ts +357 -0
- package/extensions/tools/initiatives.ts +328 -0
- package/extensions/tools/issue-labels.ts +273 -0
- package/extensions/tools/issue-relations.ts +207 -0
- package/extensions/tools/issue-statuses.ts +72 -0
- package/extensions/tools/issues.ts +674 -0
- package/extensions/tools/milestones.ts +250 -0
- package/extensions/tools/project-labels.ts +227 -0
- package/extensions/tools/project-relations.ts +219 -0
- package/extensions/tools/projects.ts +365 -0
- package/extensions/tools/teams.ts +107 -0
- package/extensions/tools/users.ts +107 -0
- package/extensions/tools/workspaces.ts +33 -0
- package/extensions/types.ts +31 -0
- package/extensions/util.ts +38 -0
- package/package.json +38 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { defineTool } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { withLinearAuth, linearGraphQL } from '../client';
|
|
4
|
+
import { PaginationParams, FilterParam, SortParam, RawInputParam } from '../params';
|
|
5
|
+
import { PROJECT_SELECTION } from '../selections';
|
|
6
|
+
import type { JsonObject } from '../types';
|
|
7
|
+
import { compactObject, asObject, asObjectArray, asString } from '../util';
|
|
8
|
+
|
|
9
|
+
export function projectTools() {
|
|
10
|
+
return [
|
|
11
|
+
defineTool({
|
|
12
|
+
name: 'linear_list_projects',
|
|
13
|
+
label: 'Linear List Projects',
|
|
14
|
+
description: 'List projects. Supports full projects query args and raw filter/sort.',
|
|
15
|
+
parameters: Type.Object({
|
|
16
|
+
...PaginationParams,
|
|
17
|
+
...FilterParam,
|
|
18
|
+
...SortParam,
|
|
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 ?? 20,
|
|
27
|
+
includeArchived: params.includeArchived,
|
|
28
|
+
last: params.last,
|
|
29
|
+
orderBy: params.orderBy,
|
|
30
|
+
sort: asObjectArray(params.sort),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const data = await linearGraphQL<{
|
|
34
|
+
projects: { nodes: Array<JsonObject> };
|
|
35
|
+
}>(
|
|
36
|
+
apiKey,
|
|
37
|
+
`query ListProjects(
|
|
38
|
+
$after: String
|
|
39
|
+
$before: String
|
|
40
|
+
$filter: ProjectFilter
|
|
41
|
+
$first: Int
|
|
42
|
+
$includeArchived: Boolean
|
|
43
|
+
$last: Int
|
|
44
|
+
$orderBy: PaginationOrderBy
|
|
45
|
+
$sort: [ProjectSortInput!]
|
|
46
|
+
) {
|
|
47
|
+
projects(
|
|
48
|
+
after: $after
|
|
49
|
+
before: $before
|
|
50
|
+
filter: $filter
|
|
51
|
+
first: $first
|
|
52
|
+
includeArchived: $includeArchived
|
|
53
|
+
last: $last
|
|
54
|
+
orderBy: $orderBy
|
|
55
|
+
sort: $sort
|
|
56
|
+
) {
|
|
57
|
+
nodes {
|
|
58
|
+
${PROJECT_SELECTION}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}`,
|
|
62
|
+
variables,
|
|
63
|
+
signal,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const projects = data.projects.nodes;
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: 'text', text: JSON.stringify({ projects }, null, 2) }],
|
|
69
|
+
details: { projects },
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
defineTool({
|
|
75
|
+
name: 'linear_get_project',
|
|
76
|
+
label: 'Linear Get Project',
|
|
77
|
+
description: 'Get a specific project by id.',
|
|
78
|
+
parameters: Type.Object({
|
|
79
|
+
projectId: Type.String({ description: 'Project id.' }),
|
|
80
|
+
}),
|
|
81
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
82
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
83
|
+
const data = await linearGraphQL<{ project: JsonObject | null }>(
|
|
84
|
+
apiKey,
|
|
85
|
+
`query GetProject($id: String!) {
|
|
86
|
+
project(id: $id) {
|
|
87
|
+
${PROJECT_SELECTION}
|
|
88
|
+
}
|
|
89
|
+
}`,
|
|
90
|
+
{ id: params.projectId },
|
|
91
|
+
signal,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const project = data.project;
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{ type: 'text', text: JSON.stringify({ project: project ?? null }, null, 2) },
|
|
98
|
+
],
|
|
99
|
+
details: { project: project ?? null },
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
defineTool({
|
|
105
|
+
name: 'linear_save_project',
|
|
106
|
+
label: 'Linear Save Project',
|
|
107
|
+
description:
|
|
108
|
+
'Create or update a project. If projectId/id is provided, uses projectUpdate; otherwise uses projectCreate.',
|
|
109
|
+
parameters: Type.Object({
|
|
110
|
+
projectId: Type.Optional(Type.String({ description: 'Project id for update mode.' })),
|
|
111
|
+
id: Type.Optional(Type.String({ description: 'ProjectCreateInput.id' })),
|
|
112
|
+
name: Type.Optional(Type.String()),
|
|
113
|
+
description: Type.Optional(Type.String()),
|
|
114
|
+
content: Type.Optional(Type.String()),
|
|
115
|
+
color: Type.Optional(Type.String()),
|
|
116
|
+
icon: Type.Optional(Type.String()),
|
|
117
|
+
convertedFromIssueId: Type.Optional(Type.String()),
|
|
118
|
+
labelIds: Type.Optional(Type.Array(Type.String())),
|
|
119
|
+
lastAppliedTemplateId: Type.Optional(Type.String()),
|
|
120
|
+
leadId: Type.Optional(Type.String()),
|
|
121
|
+
memberIds: Type.Optional(Type.Array(Type.String())),
|
|
122
|
+
priority: Type.Optional(Type.Number()),
|
|
123
|
+
prioritySortOrder: Type.Optional(Type.Number()),
|
|
124
|
+
sortOrder: Type.Optional(Type.Number()),
|
|
125
|
+
startDate: Type.Optional(Type.String()),
|
|
126
|
+
startDateResolution: Type.Optional(Type.String()),
|
|
127
|
+
statusId: Type.Optional(Type.String()),
|
|
128
|
+
targetDate: Type.Optional(Type.String()),
|
|
129
|
+
targetDateResolution: Type.Optional(Type.String()),
|
|
130
|
+
teamIds: Type.Optional(Type.Array(Type.String())),
|
|
131
|
+
templateId: Type.Optional(Type.String()),
|
|
132
|
+
useDefaultTemplate: Type.Optional(Type.Boolean()),
|
|
133
|
+
canceledAt: Type.Optional(Type.String()),
|
|
134
|
+
completedAt: Type.Optional(Type.String()),
|
|
135
|
+
frequencyResolution: Type.Optional(Type.String()),
|
|
136
|
+
projectUpdateRemindersPausedUntilAt: Type.Optional(Type.String()),
|
|
137
|
+
slackIssueComments: Type.Optional(Type.Boolean()),
|
|
138
|
+
slackIssueStatuses: Type.Optional(Type.Boolean()),
|
|
139
|
+
slackNewIssue: Type.Optional(Type.Boolean()),
|
|
140
|
+
trashed: Type.Optional(Type.Boolean()),
|
|
141
|
+
updateReminderFrequency: Type.Optional(Type.Number()),
|
|
142
|
+
updateReminderFrequencyInWeeks: Type.Optional(Type.Number()),
|
|
143
|
+
updateRemindersDay: Type.Optional(Type.String()),
|
|
144
|
+
updateRemindersHour: Type.Optional(Type.Integer()),
|
|
145
|
+
slackChannelName: Type.Optional(Type.String()),
|
|
146
|
+
...RawInputParam,
|
|
147
|
+
}),
|
|
148
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
149
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
150
|
+
const rawInput = asObject(params.input) || {};
|
|
151
|
+
const updateId = asString(params.projectId) || asString(rawInput.id);
|
|
152
|
+
|
|
153
|
+
const input = {
|
|
154
|
+
...rawInput,
|
|
155
|
+
...compactObject({
|
|
156
|
+
canceledAt: params.canceledAt,
|
|
157
|
+
color: params.color,
|
|
158
|
+
completedAt: params.completedAt,
|
|
159
|
+
content: params.content,
|
|
160
|
+
convertedFromIssueId: params.convertedFromIssueId,
|
|
161
|
+
description: params.description,
|
|
162
|
+
frequencyResolution: params.frequencyResolution,
|
|
163
|
+
icon: params.icon,
|
|
164
|
+
id: params.id,
|
|
165
|
+
labelIds: params.labelIds,
|
|
166
|
+
lastAppliedTemplateId: params.lastAppliedTemplateId,
|
|
167
|
+
leadId: params.leadId,
|
|
168
|
+
memberIds: params.memberIds,
|
|
169
|
+
name: params.name,
|
|
170
|
+
priority: params.priority,
|
|
171
|
+
prioritySortOrder: params.prioritySortOrder,
|
|
172
|
+
projectUpdateRemindersPausedUntilAt: params.projectUpdateRemindersPausedUntilAt,
|
|
173
|
+
slackIssueComments: params.slackIssueComments,
|
|
174
|
+
slackIssueStatuses: params.slackIssueStatuses,
|
|
175
|
+
slackNewIssue: params.slackNewIssue,
|
|
176
|
+
sortOrder: params.sortOrder,
|
|
177
|
+
startDate: params.startDate,
|
|
178
|
+
startDateResolution: params.startDateResolution,
|
|
179
|
+
statusId: params.statusId,
|
|
180
|
+
targetDate: params.targetDate,
|
|
181
|
+
targetDateResolution: params.targetDateResolution,
|
|
182
|
+
teamIds: params.teamIds,
|
|
183
|
+
templateId: params.templateId,
|
|
184
|
+
trashed: params.trashed,
|
|
185
|
+
updateReminderFrequency: params.updateReminderFrequency,
|
|
186
|
+
updateReminderFrequencyInWeeks: params.updateReminderFrequencyInWeeks,
|
|
187
|
+
updateRemindersDay: params.updateRemindersDay,
|
|
188
|
+
updateRemindersHour: params.updateRemindersHour,
|
|
189
|
+
useDefaultTemplate: params.useDefaultTemplate,
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (updateId) {
|
|
194
|
+
if (Object.keys(input).length === 0) {
|
|
195
|
+
throw new Error('No project update fields were provided.');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const data = await linearGraphQL<{
|
|
199
|
+
projectUpdate: { success: boolean; project?: JsonObject | null };
|
|
200
|
+
}>(
|
|
201
|
+
apiKey,
|
|
202
|
+
`mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {
|
|
203
|
+
projectUpdate(id: $id, input: $input) {
|
|
204
|
+
success
|
|
205
|
+
project {
|
|
206
|
+
${PROJECT_SELECTION}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}`,
|
|
210
|
+
{ id: updateId, input },
|
|
211
|
+
signal,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!data.projectUpdate.success || !data.projectUpdate.project) {
|
|
215
|
+
throw new Error('Linear projectUpdate did not succeed.');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const project = data.projectUpdate.project;
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: 'text', text: JSON.stringify({ project }, null, 2) }],
|
|
221
|
+
details: { project },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!asString(input.name)) {
|
|
226
|
+
throw new Error('Project name is required for projectCreate (name).');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!Array.isArray(input.teamIds) || input.teamIds.length === 0) {
|
|
230
|
+
throw new Error('teamIds is required for projectCreate and must be a non-empty array.');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const data = await linearGraphQL<{
|
|
234
|
+
projectCreate: { success: boolean; project?: JsonObject | null };
|
|
235
|
+
}>(
|
|
236
|
+
apiKey,
|
|
237
|
+
`mutation CreateProject($input: ProjectCreateInput!, $slackChannelName: String) {
|
|
238
|
+
projectCreate(input: $input, slackChannelName: $slackChannelName) {
|
|
239
|
+
success
|
|
240
|
+
project {
|
|
241
|
+
${PROJECT_SELECTION}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}`,
|
|
245
|
+
{
|
|
246
|
+
input,
|
|
247
|
+
slackChannelName: params.slackChannelName,
|
|
248
|
+
},
|
|
249
|
+
signal,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
if (!data.projectCreate.success || !data.projectCreate.project) {
|
|
253
|
+
throw new Error('Linear projectCreate did not succeed.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const project = data.projectCreate.project;
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: 'text', text: JSON.stringify({ project }, null, 2) }],
|
|
259
|
+
details: { project },
|
|
260
|
+
};
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
264
|
+
defineTool({
|
|
265
|
+
name: 'linear_delete_project',
|
|
266
|
+
label: 'Linear Delete Project',
|
|
267
|
+
description: 'Delete a project by id.',
|
|
268
|
+
parameters: Type.Object({
|
|
269
|
+
projectId: Type.String(),
|
|
270
|
+
}),
|
|
271
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
272
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
273
|
+
const data = await linearGraphQL<{
|
|
274
|
+
projectDelete: { success: boolean };
|
|
275
|
+
}>(
|
|
276
|
+
apiKey,
|
|
277
|
+
`mutation DeleteProject($id: String!) {
|
|
278
|
+
projectDelete(id: $id) {
|
|
279
|
+
success
|
|
280
|
+
}
|
|
281
|
+
}`,
|
|
282
|
+
{ id: params.projectId },
|
|
283
|
+
signal,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (!data.projectDelete.success) {
|
|
287
|
+
throw new Error('Linear projectDelete did not succeed.');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
|
|
292
|
+
details: { success: true },
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
defineTool({
|
|
298
|
+
name: 'linear_archive_project',
|
|
299
|
+
label: 'Linear Archive Project',
|
|
300
|
+
description: 'Archive a project by id. Use trash=true to trash instead.',
|
|
301
|
+
parameters: Type.Object({
|
|
302
|
+
projectId: Type.String(),
|
|
303
|
+
trash: Type.Optional(Type.Boolean()),
|
|
304
|
+
}),
|
|
305
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
306
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
307
|
+
const data = await linearGraphQL<{
|
|
308
|
+
projectArchive: { success: boolean };
|
|
309
|
+
}>(
|
|
310
|
+
apiKey,
|
|
311
|
+
`mutation ArchiveProject($id: String!, $trash: Boolean) {
|
|
312
|
+
projectArchive(id: $id, trash: $trash) {
|
|
313
|
+
success
|
|
314
|
+
}
|
|
315
|
+
}`,
|
|
316
|
+
{ id: params.projectId, trash: params.trash },
|
|
317
|
+
signal,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (!data.projectArchive.success) {
|
|
321
|
+
throw new Error('Linear projectArchive did not succeed.');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
|
|
326
|
+
details: { success: true },
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
}),
|
|
331
|
+
defineTool({
|
|
332
|
+
name: 'linear_unarchive_project',
|
|
333
|
+
label: 'Linear Unarchive Project',
|
|
334
|
+
description: 'Unarchive a project by id.',
|
|
335
|
+
parameters: Type.Object({
|
|
336
|
+
projectId: Type.String(),
|
|
337
|
+
}),
|
|
338
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
339
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
340
|
+
const data = await linearGraphQL<{
|
|
341
|
+
projectUnarchive: { success: boolean };
|
|
342
|
+
}>(
|
|
343
|
+
apiKey,
|
|
344
|
+
`mutation UnarchiveProject($id: String!) {
|
|
345
|
+
projectUnarchive(id: $id) {
|
|
346
|
+
success
|
|
347
|
+
}
|
|
348
|
+
}`,
|
|
349
|
+
{ id: params.projectId },
|
|
350
|
+
signal,
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (!data.projectUnarchive.success) {
|
|
354
|
+
throw new Error('Linear projectUnarchive did not succeed.');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: 'text', text: JSON.stringify({ success: true }, null, 2) }],
|
|
359
|
+
details: { success: true },
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
}),
|
|
364
|
+
];
|
|
365
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { TEAM_SELECTION } from '../selections';
|
|
6
|
+
import type { LinearTeam, JsonObject } from '../types';
|
|
7
|
+
import { compactObject, asObject } from '../util';
|
|
8
|
+
|
|
9
|
+
export function teamTools() {
|
|
10
|
+
return [
|
|
11
|
+
defineTool({
|
|
12
|
+
name: 'linear_list_teams',
|
|
13
|
+
label: 'Linear List Teams',
|
|
14
|
+
description:
|
|
15
|
+
'List Linear teams and states. Supports full teams query args: after, before, filter, first, includeArchived, last, orderBy.',
|
|
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,
|
|
27
|
+
includeArchived: params.includeArchived,
|
|
28
|
+
last: params.last,
|
|
29
|
+
orderBy: params.orderBy,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const data = await linearGraphQL<{ teams: { nodes: LinearTeam[] } }>(
|
|
33
|
+
apiKey,
|
|
34
|
+
`query ListTeams(
|
|
35
|
+
$after: String
|
|
36
|
+
$before: String
|
|
37
|
+
$filter: TeamFilter
|
|
38
|
+
$first: Int
|
|
39
|
+
$includeArchived: Boolean
|
|
40
|
+
$last: Int
|
|
41
|
+
$orderBy: PaginationOrderBy
|
|
42
|
+
) {
|
|
43
|
+
teams(
|
|
44
|
+
after: $after
|
|
45
|
+
before: $before
|
|
46
|
+
filter: $filter
|
|
47
|
+
first: $first
|
|
48
|
+
includeArchived: $includeArchived
|
|
49
|
+
last: $last
|
|
50
|
+
orderBy: $orderBy
|
|
51
|
+
) {
|
|
52
|
+
nodes {
|
|
53
|
+
id
|
|
54
|
+
key
|
|
55
|
+
name
|
|
56
|
+
states(first: 50) {
|
|
57
|
+
nodes {
|
|
58
|
+
id
|
|
59
|
+
name
|
|
60
|
+
type
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}`,
|
|
66
|
+
variables,
|
|
67
|
+
signal,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const teams = data.teams.nodes;
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: 'text', text: JSON.stringify({ teams }, null, 2) }],
|
|
73
|
+
details: { teams },
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
defineTool({
|
|
79
|
+
name: 'linear_get_team',
|
|
80
|
+
label: 'Linear Get Team',
|
|
81
|
+
description: 'Get a specific team by id.',
|
|
82
|
+
parameters: Type.Object({
|
|
83
|
+
teamId: Type.String(),
|
|
84
|
+
}),
|
|
85
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
86
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
87
|
+
const data = await linearGraphQL<{ team: JsonObject | null }>(
|
|
88
|
+
apiKey,
|
|
89
|
+
`query GetTeam($id: String!) {
|
|
90
|
+
team(id: $id) {
|
|
91
|
+
${TEAM_SELECTION}
|
|
92
|
+
}
|
|
93
|
+
}`,
|
|
94
|
+
{ id: params.teamId },
|
|
95
|
+
signal,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const team = data.team;
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify({ team }, null, 2) }],
|
|
101
|
+
details: { team },
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { defineTool } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { withLinearAuth, linearGraphQL } from '../client';
|
|
4
|
+
import { PaginationParams, FilterParam, SortParam } from '../params';
|
|
5
|
+
import { USER_SELECTION } from '../selections';
|
|
6
|
+
import type { JsonObject } from '../types';
|
|
7
|
+
import { compactObject, asObject, asObjectArray } from '../util';
|
|
8
|
+
|
|
9
|
+
export function userTools() {
|
|
10
|
+
return [
|
|
11
|
+
defineTool({
|
|
12
|
+
name: 'linear_list_users',
|
|
13
|
+
label: 'Linear List Users',
|
|
14
|
+
description: 'List users. Supports full users query args.',
|
|
15
|
+
parameters: Type.Object({
|
|
16
|
+
...PaginationParams,
|
|
17
|
+
...FilterParam,
|
|
18
|
+
...SortParam,
|
|
19
|
+
includeDisabled: Type.Optional(Type.Boolean()),
|
|
20
|
+
}),
|
|
21
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
22
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
23
|
+
const variables = compactObject({
|
|
24
|
+
after: params.after,
|
|
25
|
+
before: params.before,
|
|
26
|
+
filter: asObject(params.filter),
|
|
27
|
+
first: params.first ?? 50,
|
|
28
|
+
includeArchived: params.includeArchived,
|
|
29
|
+
includeDisabled: params.includeDisabled,
|
|
30
|
+
last: params.last,
|
|
31
|
+
orderBy: params.orderBy,
|
|
32
|
+
sort: asObjectArray(params.sort),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const data = await linearGraphQL<{
|
|
36
|
+
users: { nodes: Array<JsonObject> };
|
|
37
|
+
}>(
|
|
38
|
+
apiKey,
|
|
39
|
+
`query ListUsers(
|
|
40
|
+
$after: String
|
|
41
|
+
$before: String
|
|
42
|
+
$filter: UserFilter
|
|
43
|
+
$first: Int
|
|
44
|
+
$includeArchived: Boolean
|
|
45
|
+
$includeDisabled: Boolean
|
|
46
|
+
$last: Int
|
|
47
|
+
$orderBy: PaginationOrderBy
|
|
48
|
+
$sort: [UserSortInput!]
|
|
49
|
+
) {
|
|
50
|
+
users(
|
|
51
|
+
after: $after
|
|
52
|
+
before: $before
|
|
53
|
+
filter: $filter
|
|
54
|
+
first: $first
|
|
55
|
+
includeArchived: $includeArchived
|
|
56
|
+
includeDisabled: $includeDisabled
|
|
57
|
+
last: $last
|
|
58
|
+
orderBy: $orderBy
|
|
59
|
+
sort: $sort
|
|
60
|
+
) {
|
|
61
|
+
nodes {
|
|
62
|
+
${USER_SELECTION}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}`,
|
|
66
|
+
variables,
|
|
67
|
+
signal,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const users = data.users.nodes;
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: 'text', text: JSON.stringify({ users }, null, 2) }],
|
|
73
|
+
details: { users },
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
defineTool({
|
|
79
|
+
name: 'linear_get_user',
|
|
80
|
+
label: 'Linear Get User',
|
|
81
|
+
description: 'Get a specific user by id.',
|
|
82
|
+
parameters: Type.Object({
|
|
83
|
+
userId: Type.String(),
|
|
84
|
+
}),
|
|
85
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
86
|
+
return withLinearAuth(ctx, signal, async (apiKey) => {
|
|
87
|
+
const data = await linearGraphQL<{ user: JsonObject | null }>(
|
|
88
|
+
apiKey,
|
|
89
|
+
`query GetUser($id: String!) {
|
|
90
|
+
user(id: $id) {
|
|
91
|
+
${USER_SELECTION}
|
|
92
|
+
}
|
|
93
|
+
}`,
|
|
94
|
+
{ id: params.userId },
|
|
95
|
+
signal,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const user = data.user;
|
|
99
|
+
return {
|
|
100
|
+
content: [{ type: 'text', text: JSON.stringify({ user }, null, 2) }],
|
|
101
|
+
details: { user },
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineTool } from '@mariozechner/pi-coding-agent';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
3
|
+
import { switchWorkspace, type WorkspaceCredentials } from '../client';
|
|
4
|
+
|
|
5
|
+
export function workspaceTools(creds: WorkspaceCredentials) {
|
|
6
|
+
const names = Object.keys(creds.workspaces);
|
|
7
|
+
if (names.length < 2) return [];
|
|
8
|
+
|
|
9
|
+
return [
|
|
10
|
+
defineTool({
|
|
11
|
+
name: 'linear_switch_workspace',
|
|
12
|
+
label: 'Linear Switch Workspace',
|
|
13
|
+
description: `Switch active Linear workspace. Available: ${names.join(', ')}. Currently active: ${creds.activeWorkspace || 'none'}.`,
|
|
14
|
+
parameters: Type.Object({
|
|
15
|
+
name: Type.String({
|
|
16
|
+
description: `Workspace name to switch to. One of: ${names.join(', ')}`,
|
|
17
|
+
}),
|
|
18
|
+
}),
|
|
19
|
+
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
20
|
+
const updated = await switchWorkspace(params.name);
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: JSON.stringify({ active: updated.activeWorkspace }, null, 2),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
details: { active: updated.activeWorkspace },
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type JsonObject = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
export type LinearGraphQLError = {
|
|
4
|
+
message: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type LinearIssue = {
|
|
8
|
+
id: string;
|
|
9
|
+
identifier: string;
|
|
10
|
+
number?: number | null;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
priority?: number | null;
|
|
14
|
+
url?: string | null;
|
|
15
|
+
branchName?: string | null;
|
|
16
|
+
dueDate?: string | null;
|
|
17
|
+
createdAt?: string;
|
|
18
|
+
updatedAt?: string;
|
|
19
|
+
state?: { id: string; name: string; type?: string | null } | null;
|
|
20
|
+
team?: { id: string; key: string; name: string } | null;
|
|
21
|
+
assignee?: { id: string; name?: string | null; email?: string | null } | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type LinearTeam = {
|
|
25
|
+
id: string;
|
|
26
|
+
key: string;
|
|
27
|
+
name: string;
|
|
28
|
+
states?: {
|
|
29
|
+
nodes: Array<{ id: string; name: string; type?: string | null }>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Type } from '@sinclair/typebox';
|
|
2
|
+
import type { JsonObject } from './types';
|
|
3
|
+
|
|
4
|
+
export function asString(value: unknown): string | undefined {
|
|
5
|
+
if (typeof value !== 'string') return undefined;
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
return trimmed ? trimmed : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function asObject(value: unknown): JsonObject | undefined {
|
|
11
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
|
|
12
|
+
return value as JsonObject;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function asObjectArray(value: unknown): JsonObject[] | undefined {
|
|
16
|
+
if (!Array.isArray(value)) return undefined;
|
|
17
|
+
const mapped = value.map(asObject).filter((item): item is JsonObject => Boolean(item));
|
|
18
|
+
return mapped.length ? mapped : [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function compactObject<T extends Record<string, unknown>>(input: T): Partial<T> {
|
|
22
|
+
const output: Partial<T> = {};
|
|
23
|
+
for (const [key, value] of Object.entries(input)) {
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
output[key as keyof T] = value as T[keyof T];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return output;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function mergeFilters(base?: JsonObject, extra?: JsonObject): JsonObject | undefined {
|
|
32
|
+
if (base && extra) {
|
|
33
|
+
return { and: [base, extra] };
|
|
34
|
+
}
|
|
35
|
+
return base || extra;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const GenericObjectSchema = Type.Record(Type.String(), Type.Any());
|