@yannelli/paseo-linear-plugin 0.1.0 → 0.3.0
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/NOTICE +8 -4
- package/OVERVIEW.md +22 -13
- package/README.md +134 -29
- package/client/agent-options.ts +200 -0
- package/client/breadcrumbs.tsx +110 -0
- package/client/browser.tsx +275 -0
- package/client/compat.ts +107 -0
- package/client/connect.tsx +136 -0
- package/client/create-issue.tsx +291 -0
- package/client/glyphs.tsx +117 -0
- package/client/issue-detail.tsx +366 -0
- package/client/issue-list.tsx +373 -0
- package/client/issue-row.tsx +152 -0
- package/client/issue-sections.tsx +325 -0
- package/client/issue-tree.ts +132 -0
- package/client/key-scope.tsx +23 -0
- package/client/launch-choices.ts +125 -0
- package/client/launch-fields.tsx +152 -0
- package/client/launch-plan.ts +227 -0
- package/client/launch.tsx +484 -0
- package/client/markdown.tsx +240 -0
- package/client/model-browser.tsx +296 -0
- package/client/pickers.tsx +238 -0
- package/client/provider-icon.tsx +78 -0
- package/client/queries.ts +211 -0
- package/client/settings-connection.tsx +109 -0
- package/client/settings-fields.tsx +173 -0
- package/client/settings-keys.tsx +117 -0
- package/client/settings-projects.tsx +281 -0
- package/client/settings-prompts.tsx +198 -0
- package/client/store.ts +71 -0
- package/client/timeline-card.tsx +63 -0
- package/client/ui.tsx +352 -0
- package/index.client.tsx +111 -0
- package/index.server.ts +7 -3
- package/package.json +4 -3
- package/paseo-plugin.json +2 -2
- package/server/cache.ts +68 -0
- package/server/credentials.ts +101 -0
- package/server/graphql.ts +68 -0
- package/server/handlers.ts +172 -0
- package/server/linear.ts +11 -64
- package/server/queries.ts +280 -0
- package/shared/linear.ts +268 -0
- package/shared/markdown.ts +416 -0
- package/shared/prompts.ts +150 -0
- package/shared/settings.ts +68 -0
- package/index.client.ts +0 -7
- package/server/issues.ts +0 -10
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
type IssueDetail,
|
|
4
|
+
type IssuePatch,
|
|
5
|
+
type IssueQuery,
|
|
6
|
+
type IssueRef,
|
|
7
|
+
type IssueSort,
|
|
8
|
+
type IssueSummary,
|
|
9
|
+
LINEAR_IDENTIFIER,
|
|
10
|
+
STATUS_STATE_TYPES,
|
|
11
|
+
UserSchema,
|
|
12
|
+
WorkflowStateSchema,
|
|
13
|
+
} from "../shared/linear";
|
|
14
|
+
import type { LinearGraphql } from "./graphql";
|
|
15
|
+
import { LinearApiError } from "./graphql";
|
|
16
|
+
|
|
17
|
+
const USER_FIELDS = "id name displayName avatarUrl";
|
|
18
|
+
const STATE_FIELDS = "id name type color position";
|
|
19
|
+
const REF_FIELDS = "id identifier title";
|
|
20
|
+
const SUMMARY_FIELDS = `
|
|
21
|
+
id identifier title url branchName priority priorityLabel updatedAt
|
|
22
|
+
state { ${STATE_FIELDS} }
|
|
23
|
+
assignee { ${USER_FIELDS} }
|
|
24
|
+
team { id key name }
|
|
25
|
+
project { id name }
|
|
26
|
+
labels(first: 20) { nodes { id name color } }
|
|
27
|
+
parent { ${REF_FIELDS} }
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
// Breadcrumbs show this many parent levels. Deeper chains start at the highest fetched level.
|
|
31
|
+
export const ANCESTOR_DEPTH = 6;
|
|
32
|
+
const ANCESTOR_FIELDS = Array.from({ length: ANCESTOR_DEPTH }).reduce<string>(
|
|
33
|
+
(inner) => `parent { ${REF_FIELDS} ${inner} }`,
|
|
34
|
+
"",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
export const ISSUE_SORT_INPUT: Record<IssueSort, Record<string, unknown>[]> = {
|
|
38
|
+
updated: [{ updatedAt: { order: "Descending" } }],
|
|
39
|
+
created: [{ createdAt: { order: "Descending" } }],
|
|
40
|
+
priority: [{ priority: { order: "Descending" } }, { updatedAt: { order: "Descending" } }],
|
|
41
|
+
due: [{ dueDate: { order: "Ascending", nulls: "last" } }, { updatedAt: { order: "Descending" } }],
|
|
42
|
+
title: [{ title: { order: "Ascending" } }],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Linear caps query complexity (connection sizes multiply), so the catalog asks only for
|
|
46
|
+
// what the UI uses: teams with their workflow states, and active users.
|
|
47
|
+
export const CATALOG_QUERY = `
|
|
48
|
+
query PaseoLinearCatalog {
|
|
49
|
+
viewer { ${USER_FIELDS} }
|
|
50
|
+
organization { name urlKey }
|
|
51
|
+
teams(first: 50) {
|
|
52
|
+
nodes {
|
|
53
|
+
id key name
|
|
54
|
+
states(first: 50) { nodes { ${STATE_FIELDS} } }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
users(first: 250, filter: { active: { eq: true } }) { nodes { ${USER_FIELDS} } }
|
|
58
|
+
}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export const LIST_QUERY = `
|
|
62
|
+
query PaseoLinearIssueList($filter: IssueFilter, $after: String, $sort: [IssueSortInput!]) {
|
|
63
|
+
issues(first: 50, after: $after, filter: $filter, sort: $sort) {
|
|
64
|
+
nodes { ${SUMMARY_FIELDS} }
|
|
65
|
+
pageInfo { hasNextPage endCursor }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
export const DETAIL_QUERY = `
|
|
71
|
+
query PaseoLinearIssueDetail($id: String!) {
|
|
72
|
+
issue(id: $id) {
|
|
73
|
+
${SUMMARY_FIELDS}
|
|
74
|
+
description createdAt
|
|
75
|
+
creator { ${USER_FIELDS} }
|
|
76
|
+
ancestors: ${ANCESTOR_FIELDS}
|
|
77
|
+
children(first: 50) { nodes { id identifier title state { ${STATE_FIELDS} } } }
|
|
78
|
+
comments(first: 100) { nodes { id body createdAt user { ${USER_FIELDS} } } }
|
|
79
|
+
attachments(first: 25) { nodes { id title subtitle url sourceType } }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export const UPDATE_MUTATION = `
|
|
85
|
+
mutation PaseoLinearIssueUpdate($id: String!, $input: IssueUpdateInput!) {
|
|
86
|
+
issueUpdate(id: $id, input: $input) { success issue { ${SUMMARY_FIELDS} } }
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
export const CREATE_MUTATION = `
|
|
91
|
+
mutation PaseoLinearIssueCreate($input: IssueCreateInput!) {
|
|
92
|
+
issueCreate(input: $input) { success issue { ${SUMMARY_FIELDS} } }
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
export const COMMENT_MUTATION = `
|
|
97
|
+
mutation PaseoLinearCommentCreate($input: CommentCreateInput!) {
|
|
98
|
+
commentCreate(input: $input) { success comment { id body createdAt user { ${USER_FIELDS} } } }
|
|
99
|
+
}
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
const nodes = <T extends z.ZodType>(schema: T) =>
|
|
103
|
+
z.object({ nodes: z.array(schema) }).transform((connection) => connection.nodes);
|
|
104
|
+
const LabelRaw = z.object({ id: z.string(), name: z.string(), color: z.string() });
|
|
105
|
+
const RefRaw = z.object({ id: z.string(), identifier: z.string(), title: z.string() });
|
|
106
|
+
type RawAncestor = IssueRef & { parent?: RawAncestor | null };
|
|
107
|
+
const RawAncestorSchema: z.ZodType<RawAncestor> = z.lazy(() =>
|
|
108
|
+
RefRaw.extend({ parent: RawAncestorSchema.nullable().optional() }),
|
|
109
|
+
);
|
|
110
|
+
const RawSummary = z.object({
|
|
111
|
+
id: z.string(),
|
|
112
|
+
identifier: z.string(),
|
|
113
|
+
title: z.string(),
|
|
114
|
+
url: z.string(),
|
|
115
|
+
branchName: z.string(),
|
|
116
|
+
priority: z.number(),
|
|
117
|
+
priorityLabel: z.string(),
|
|
118
|
+
updatedAt: z.string(),
|
|
119
|
+
state: WorkflowStateSchema,
|
|
120
|
+
assignee: UserSchema.nullable(),
|
|
121
|
+
team: z.object({ id: z.string(), key: z.string(), name: z.string() }),
|
|
122
|
+
project: z.object({ id: z.string(), name: z.string() }).nullable(),
|
|
123
|
+
labels: nodes(LabelRaw),
|
|
124
|
+
parent: RefRaw.nullable(),
|
|
125
|
+
});
|
|
126
|
+
const RawComment = z.object({
|
|
127
|
+
id: z.string(),
|
|
128
|
+
body: z.string(),
|
|
129
|
+
createdAt: z.string(),
|
|
130
|
+
user: UserSchema.nullable(),
|
|
131
|
+
});
|
|
132
|
+
const RawDetail = RawSummary.extend({
|
|
133
|
+
description: z.string().nullable(),
|
|
134
|
+
createdAt: z.string(),
|
|
135
|
+
creator: UserSchema.nullable(),
|
|
136
|
+
ancestors: RawAncestorSchema.nullable(),
|
|
137
|
+
children: nodes(
|
|
138
|
+
z.object({
|
|
139
|
+
id: z.string(),
|
|
140
|
+
identifier: z.string(),
|
|
141
|
+
title: z.string(),
|
|
142
|
+
state: WorkflowStateSchema,
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
comments: nodes(RawComment),
|
|
146
|
+
attachments: nodes(
|
|
147
|
+
z.object({
|
|
148
|
+
id: z.string(),
|
|
149
|
+
title: z.string(),
|
|
150
|
+
subtitle: z.string().nullable(),
|
|
151
|
+
url: z.string(),
|
|
152
|
+
sourceType: z.string().nullable(),
|
|
153
|
+
}),
|
|
154
|
+
),
|
|
155
|
+
});
|
|
156
|
+
const CatalogResponse = z.object({
|
|
157
|
+
viewer: UserSchema,
|
|
158
|
+
organization: z.object({ name: z.string(), urlKey: z.string() }),
|
|
159
|
+
teams: nodes(
|
|
160
|
+
z.object({
|
|
161
|
+
id: z.string(),
|
|
162
|
+
key: z.string(),
|
|
163
|
+
name: z.string(),
|
|
164
|
+
states: nodes(WorkflowStateSchema),
|
|
165
|
+
}),
|
|
166
|
+
),
|
|
167
|
+
users: nodes(UserSchema),
|
|
168
|
+
});
|
|
169
|
+
const ListResponse = z.object({
|
|
170
|
+
issues: z.object({
|
|
171
|
+
nodes: z.array(RawSummary),
|
|
172
|
+
pageInfo: z.object({ hasNextPage: z.boolean(), endCursor: z.string().nullable() }),
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
const IssuePayload = z.object({ success: z.boolean(), issue: RawSummary.nullable() });
|
|
176
|
+
|
|
177
|
+
export function issueFilter(query: IssueQuery): Record<string, unknown> | null {
|
|
178
|
+
const clauses: Record<string, unknown>[] = [];
|
|
179
|
+
if (query.teamId) clauses.push({ team: { id: { eq: query.teamId } } });
|
|
180
|
+
if (query.assignee === "me") clauses.push({ assignee: { isMe: { eq: true } } });
|
|
181
|
+
if (query.assignee === "unassigned") clauses.push({ assignee: { null: true } });
|
|
182
|
+
const types = STATUS_STATE_TYPES[query.status];
|
|
183
|
+
if (types) clauses.push({ state: { type: { in: [...types] } } });
|
|
184
|
+
const text = query.query.trim();
|
|
185
|
+
if (text) {
|
|
186
|
+
clauses.push({
|
|
187
|
+
or: [{ title: { containsIgnoreCase: text } }, { description: { containsIgnoreCase: text } }],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return clauses.length > 0 ? { and: clauses } : null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Flattens the nested parent chain into root-first order. Stops at a repeated issue. */
|
|
194
|
+
export function flattenAncestors(chain: RawAncestor | null | undefined): IssueRef[] {
|
|
195
|
+
const ancestors: IssueRef[] = [];
|
|
196
|
+
const seen = new Set<string>();
|
|
197
|
+
for (let node = chain; node && !seen.has(node.id); node = node.parent) {
|
|
198
|
+
seen.add(node.id);
|
|
199
|
+
ancestors.unshift({ id: node.id, identifier: node.identifier, title: node.title });
|
|
200
|
+
}
|
|
201
|
+
return ancestors;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sortStates<T extends { states: { position: number }[] }>(team: T): T {
|
|
205
|
+
return { ...team, states: [...team.states].sort((a, b) => a.position - b.position) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createLinearService(graphql: LinearGraphql) {
|
|
209
|
+
async function getIssue(id: string): Promise<IssueDetail | null> {
|
|
210
|
+
const data = z
|
|
211
|
+
.object({ issue: RawDetail.nullable() })
|
|
212
|
+
.parse(await graphql(DETAIL_QUERY, { id }));
|
|
213
|
+
if (!data.issue) return null;
|
|
214
|
+
const comments = [...data.issue.comments].sort((a, b) =>
|
|
215
|
+
a.createdAt.localeCompare(b.createdAt),
|
|
216
|
+
);
|
|
217
|
+
return { ...data.issue, ancestors: flattenAncestors(data.issue.ancestors), comments };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function requireIssue(payload: z.infer<typeof IssuePayload>, action: string): IssueSummary {
|
|
221
|
+
if (!payload.success || !payload.issue) throw new LinearApiError(`Linear could not ${action}`);
|
|
222
|
+
return payload.issue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
async catalog() {
|
|
227
|
+
const data = CatalogResponse.parse(await graphql(CATALOG_QUERY));
|
|
228
|
+
return { ...data, teams: data.teams.map(sortStates) };
|
|
229
|
+
},
|
|
230
|
+
async listIssues(query: IssueQuery) {
|
|
231
|
+
const text = query.query.trim();
|
|
232
|
+
if (LINEAR_IDENTIFIER.test(text) && !query.after) {
|
|
233
|
+
const issue = await getIssue(text.toUpperCase()).catch(() => null);
|
|
234
|
+
if (issue) return { issues: [issue], endCursor: null, hasNextPage: false };
|
|
235
|
+
}
|
|
236
|
+
const data = ListResponse.parse(
|
|
237
|
+
await graphql(LIST_QUERY, {
|
|
238
|
+
filter: issueFilter(query),
|
|
239
|
+
after: query.after,
|
|
240
|
+
sort: ISSUE_SORT_INPUT[query.sort ?? "updated"],
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
return {
|
|
244
|
+
issues: data.issues.nodes,
|
|
245
|
+
endCursor: data.issues.pageInfo.endCursor,
|
|
246
|
+
hasNextPage: data.issues.pageInfo.hasNextPage,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
async getIssue(id: string) {
|
|
250
|
+
const issue = await getIssue(id);
|
|
251
|
+
if (!issue) throw new LinearApiError(`Linear issue ${id} was not found`);
|
|
252
|
+
return issue;
|
|
253
|
+
},
|
|
254
|
+
async updateIssue(id: string, patch: IssuePatch) {
|
|
255
|
+
const data = z
|
|
256
|
+
.object({ issueUpdate: IssuePayload })
|
|
257
|
+
.parse(await graphql(UPDATE_MUTATION, { id, input: patch }));
|
|
258
|
+
return requireIssue(data.issueUpdate, "update the issue");
|
|
259
|
+
},
|
|
260
|
+
async createIssue(input: Record<string, unknown>) {
|
|
261
|
+
const data = z
|
|
262
|
+
.object({ issueCreate: IssuePayload })
|
|
263
|
+
.parse(await graphql(CREATE_MUTATION, { input }));
|
|
264
|
+
return requireIssue(data.issueCreate, "create the issue");
|
|
265
|
+
},
|
|
266
|
+
async createComment(issueId: string, body: string) {
|
|
267
|
+
const data = z
|
|
268
|
+
.object({
|
|
269
|
+
commentCreate: z.object({ success: z.boolean(), comment: RawComment.nullable() }),
|
|
270
|
+
})
|
|
271
|
+
.parse(await graphql(COMMENT_MUTATION, { input: { issueId, body } }));
|
|
272
|
+
if (!data.commentCreate.success || !data.commentCreate.comment) {
|
|
273
|
+
throw new LinearApiError("Linear could not add the comment");
|
|
274
|
+
}
|
|
275
|
+
return data.commentCreate.comment;
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export type LinearService = ReturnType<typeof createLinearService>;
|
package/shared/linear.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { defineRpc } from "@getpaseo/plugin";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const UserSchema = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
displayName: z.string(),
|
|
8
|
+
avatarUrl: z.string().nullable(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const WorkflowStateSchema = z.object({
|
|
12
|
+
id: z.string(),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
type: z.string(),
|
|
15
|
+
color: z.string(),
|
|
16
|
+
position: z.number(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const LabelSchema = z.object({
|
|
20
|
+
id: z.string(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
color: z.string(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const TeamSchema = z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
key: z.string(),
|
|
28
|
+
name: z.string(),
|
|
29
|
+
states: z.array(WorkflowStateSchema),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const IssueRefSchema = z.object({
|
|
33
|
+
id: z.string(),
|
|
34
|
+
identifier: z.string(),
|
|
35
|
+
title: z.string(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const IssueSummarySchema = z.object({
|
|
39
|
+
id: z.string(),
|
|
40
|
+
identifier: z.string(),
|
|
41
|
+
title: z.string(),
|
|
42
|
+
url: z.string(),
|
|
43
|
+
branchName: z.string(),
|
|
44
|
+
priority: z.number(),
|
|
45
|
+
priorityLabel: z.string(),
|
|
46
|
+
updatedAt: z.string(),
|
|
47
|
+
state: WorkflowStateSchema,
|
|
48
|
+
assignee: UserSchema.nullable(),
|
|
49
|
+
team: z.object({ id: z.string(), key: z.string(), name: z.string() }),
|
|
50
|
+
project: z.object({ id: z.string(), name: z.string() }).nullable(),
|
|
51
|
+
labels: z.array(LabelSchema),
|
|
52
|
+
parent: IssueRefSchema.nullable(),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export const CommentSchema = z.object({
|
|
56
|
+
id: z.string(),
|
|
57
|
+
body: z.string(),
|
|
58
|
+
createdAt: z.string(),
|
|
59
|
+
user: UserSchema.nullable(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const IssueAttachmentSchema = z.object({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
title: z.string(),
|
|
65
|
+
subtitle: z.string().nullable(),
|
|
66
|
+
url: z.string(),
|
|
67
|
+
sourceType: z.string().nullable(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
export const IssueDetailSchema = IssueSummarySchema.extend({
|
|
71
|
+
description: z.string().nullable(),
|
|
72
|
+
createdAt: z.string(),
|
|
73
|
+
creator: UserSchema.nullable(),
|
|
74
|
+
// Parent chain, root first. The last entry is the direct parent.
|
|
75
|
+
ancestors: z.array(IssueRefSchema),
|
|
76
|
+
children: z.array(
|
|
77
|
+
z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
identifier: z.string(),
|
|
80
|
+
title: z.string(),
|
|
81
|
+
state: WorkflowStateSchema,
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
comments: z.array(CommentSchema),
|
|
85
|
+
attachments: z.array(IssueAttachmentSchema),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export type LinearUser = z.infer<typeof UserSchema>;
|
|
89
|
+
export type WorkflowState = z.infer<typeof WorkflowStateSchema>;
|
|
90
|
+
export type LinearLabel = z.infer<typeof LabelSchema>;
|
|
91
|
+
export type LinearTeam = z.infer<typeof TeamSchema>;
|
|
92
|
+
export type IssueSummary = z.infer<typeof IssueSummarySchema>;
|
|
93
|
+
export type IssueDetail = z.infer<typeof IssueDetailSchema>;
|
|
94
|
+
export type IssueComment = z.infer<typeof CommentSchema>;
|
|
95
|
+
export type IssueRef = z.infer<typeof IssueRefSchema>;
|
|
96
|
+
|
|
97
|
+
// The Paseo project whose Linear key to use. Null or absent uses the default key.
|
|
98
|
+
const KeyScope = { projectId: z.string().min(1).nullable().optional() };
|
|
99
|
+
export type KeyScopeInput = { projectId?: string | null };
|
|
100
|
+
|
|
101
|
+
const KeySourceSchema = z.enum(["project", "file", "environment"]);
|
|
102
|
+
export type KeySource = z.infer<typeof KeySourceSchema>;
|
|
103
|
+
|
|
104
|
+
export const authStatusRpc = defineRpc({
|
|
105
|
+
name: "linear.auth.status",
|
|
106
|
+
input: z.object(KeyScope),
|
|
107
|
+
output: z.object({
|
|
108
|
+
configured: z.boolean(),
|
|
109
|
+
source: KeySourceSchema.nullable(),
|
|
110
|
+
keyHint: z.string().nullable(),
|
|
111
|
+
projectKeys: z.array(z.object({ projectId: z.string(), keyHint: z.string() })),
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const authSaveRpc = defineRpc({
|
|
116
|
+
name: "linear.auth.save",
|
|
117
|
+
input: z.object({ apiKey: z.string().trim().min(1, "Paste a Linear API key"), ...KeyScope }),
|
|
118
|
+
output: z.object({ viewerName: z.string(), organizationName: z.string() }),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export const authClearRpc = defineRpc({
|
|
122
|
+
name: "linear.auth.clear",
|
|
123
|
+
input: z.object(KeyScope),
|
|
124
|
+
output: z.object({ configured: z.boolean() }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export const CatalogSchema = z.object({
|
|
128
|
+
viewer: UserSchema,
|
|
129
|
+
organization: z.object({ name: z.string(), urlKey: z.string() }),
|
|
130
|
+
teams: z.array(TeamSchema),
|
|
131
|
+
users: z.array(UserSchema),
|
|
132
|
+
});
|
|
133
|
+
export type Catalog = z.infer<typeof CatalogSchema>;
|
|
134
|
+
|
|
135
|
+
export const catalogRpc = defineRpc({
|
|
136
|
+
name: "linear.catalog",
|
|
137
|
+
input: z.object(KeyScope),
|
|
138
|
+
output: CatalogSchema,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const ASSIGNEE_FILTERS = ["me", "anyone", "unassigned"] as const;
|
|
142
|
+
export const STATUS_FILTERS = ["active", "backlog", "done", "all"] as const;
|
|
143
|
+
export const ISSUE_SORTS = ["updated", "created", "priority", "due", "title"] as const;
|
|
144
|
+
export type AssigneeFilter = (typeof ASSIGNEE_FILTERS)[number];
|
|
145
|
+
export type StatusFilter = (typeof STATUS_FILTERS)[number];
|
|
146
|
+
export type IssueSort = (typeof ISSUE_SORTS)[number];
|
|
147
|
+
|
|
148
|
+
export const IssueQuerySchema = z.object({
|
|
149
|
+
teamId: z.string().nullable(),
|
|
150
|
+
assignee: z.enum(ASSIGNEE_FILTERS),
|
|
151
|
+
status: z.enum(STATUS_FILTERS),
|
|
152
|
+
query: z.string(),
|
|
153
|
+
sort: z.enum(ISSUE_SORTS).default("updated"),
|
|
154
|
+
after: z.string().nullable(),
|
|
155
|
+
});
|
|
156
|
+
export type IssueQuery = z.input<typeof IssueQuerySchema>;
|
|
157
|
+
|
|
158
|
+
export const IssuePageSchema = z.object({
|
|
159
|
+
issues: z.array(IssueSummarySchema),
|
|
160
|
+
endCursor: z.string().nullable(),
|
|
161
|
+
hasNextPage: z.boolean(),
|
|
162
|
+
});
|
|
163
|
+
export type IssuePage = z.infer<typeof IssuePageSchema>;
|
|
164
|
+
|
|
165
|
+
export const listIssuesRpc = defineRpc({
|
|
166
|
+
name: "linear.issues.list",
|
|
167
|
+
input: IssueQuerySchema.extend(KeyScope),
|
|
168
|
+
output: IssuePageSchema,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export const getIssueRpc = defineRpc({
|
|
172
|
+
name: "linear.issue.get",
|
|
173
|
+
input: z.object({ id: z.string().min(1), ...KeyScope }),
|
|
174
|
+
output: z.object({ issue: IssueDetailSchema }),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Stale-while-revalidate: the daemon returns its last response without calling Linear.
|
|
178
|
+
const cached = <T extends z.ZodType>(schema: T) =>
|
|
179
|
+
z.object({ value: schema.nullable(), fetchedAt: z.string().nullable() });
|
|
180
|
+
|
|
181
|
+
export const cachedCatalogRpc = defineRpc({
|
|
182
|
+
name: "linear.cache.catalog",
|
|
183
|
+
input: z.object(KeyScope),
|
|
184
|
+
output: cached(CatalogSchema),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
export const cachedIssuesRpc = defineRpc({
|
|
188
|
+
name: "linear.cache.issues",
|
|
189
|
+
input: IssueQuerySchema.extend(KeyScope),
|
|
190
|
+
output: cached(IssuePageSchema),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
export const cachedIssueRpc = defineRpc({
|
|
194
|
+
name: "linear.cache.issue",
|
|
195
|
+
input: z.object({ id: z.string().min(1), ...KeyScope }),
|
|
196
|
+
output: cached(z.object({ issue: IssueDetailSchema })),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
export const IssuePatchSchema = z.object({
|
|
200
|
+
stateId: z.string().optional(),
|
|
201
|
+
assigneeId: z.string().nullable().optional(),
|
|
202
|
+
priority: z.number().int().min(0).max(4).optional(),
|
|
203
|
+
title: z.string().trim().min(1).optional(),
|
|
204
|
+
description: z.string().optional(),
|
|
205
|
+
labelIds: z.array(z.string()).optional(),
|
|
206
|
+
});
|
|
207
|
+
export type IssuePatch = z.infer<typeof IssuePatchSchema>;
|
|
208
|
+
|
|
209
|
+
export const updateIssueRpc = defineRpc({
|
|
210
|
+
name: "linear.issue.update",
|
|
211
|
+
input: z.object({ id: z.string().min(1), patch: IssuePatchSchema, ...KeyScope }),
|
|
212
|
+
output: z.object({ issue: IssueSummarySchema }),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
export const createIssueRpc = defineRpc({
|
|
216
|
+
name: "linear.issue.create",
|
|
217
|
+
input: z.object({
|
|
218
|
+
teamId: z.string().min(1),
|
|
219
|
+
title: z.string().trim().min(1, "Enter a title"),
|
|
220
|
+
description: z.string().optional(),
|
|
221
|
+
priority: z.number().int().min(0).max(4).optional(),
|
|
222
|
+
assigneeId: z.string().nullable().optional(),
|
|
223
|
+
stateId: z.string().optional(),
|
|
224
|
+
linearProjectId: z.string().nullable().optional(),
|
|
225
|
+
...KeyScope,
|
|
226
|
+
}),
|
|
227
|
+
output: z.object({ issue: IssueSummarySchema }),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export const createCommentRpc = defineRpc({
|
|
231
|
+
name: "linear.comment.create",
|
|
232
|
+
input: z.object({ issueId: z.string().min(1), body: z.string().trim().min(1), ...KeyScope }),
|
|
233
|
+
output: z.object({ comment: CommentSchema }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
export const PRIORITIES = [
|
|
237
|
+
{ value: 0, label: "No priority", icon: "Minus" },
|
|
238
|
+
{ value: 1, label: "Urgent", icon: "TriangleAlert" },
|
|
239
|
+
{ value: 2, label: "High", icon: "SignalHigh" },
|
|
240
|
+
{ value: 3, label: "Medium", icon: "SignalMedium" },
|
|
241
|
+
{ value: 4, label: "Low", icon: "SignalLow" },
|
|
242
|
+
] as const;
|
|
243
|
+
|
|
244
|
+
export const STATUS_STATE_TYPES: Record<StatusFilter, readonly string[] | null> = {
|
|
245
|
+
active: ["unstarted", "started"],
|
|
246
|
+
backlog: ["triage", "backlog"],
|
|
247
|
+
done: ["completed", "canceled", "duplicate"],
|
|
248
|
+
all: null,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export const LINEAR_IDENTIFIER = /^[A-Z][A-Z0-9]+-\d+$/i;
|
|
252
|
+
|
|
253
|
+
export const IssueCardSchema = z.object({
|
|
254
|
+
identifier: z.string(),
|
|
255
|
+
title: z.string(),
|
|
256
|
+
url: z.string(),
|
|
257
|
+
stateName: z.string(),
|
|
258
|
+
stateColor: z.string(),
|
|
259
|
+
action: z.enum(["implement", "review"]),
|
|
260
|
+
});
|
|
261
|
+
export type IssueCard = z.infer<typeof IssueCardSchema>;
|
|
262
|
+
export const ISSUE_CARD_KIND = "linear-issue";
|
|
263
|
+
|
|
264
|
+
export const attachIssueCardRpc = defineRpc({
|
|
265
|
+
name: "linear.timeline.attach",
|
|
266
|
+
input: z.object({ agentId: z.string().min(1), card: IssueCardSchema }),
|
|
267
|
+
output: z.object({ attached: z.boolean() }),
|
|
268
|
+
});
|