runline 0.8.0 → 0.9.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.
Files changed (34) hide show
  1. package/dist/commands/auth.js +4 -2
  2. package/dist/config/loader.d.ts +2 -4
  3. package/dist/config/loader.js +2 -1
  4. package/dist/core/engine.js +18 -13
  5. package/dist/index.d.ts +2 -2
  6. package/dist/plugin/api.d.ts +2 -9
  7. package/dist/plugin/api.js +1 -4
  8. package/dist/plugin/loader.js +41 -25
  9. package/dist/plugin/schema.d.ts +19 -0
  10. package/dist/plugin/schema.js +168 -0
  11. package/dist/plugin/types.d.ts +22 -8
  12. package/dist/plugins/gmail/src/index.js +14 -2
  13. package/dist/plugins/linear/src/attachments.js +87 -0
  14. package/dist/plugins/linear/src/comments.js +64 -0
  15. package/dist/plugins/linear/src/cycles.js +42 -0
  16. package/dist/plugins/linear/src/index.js +35 -1153
  17. package/dist/plugins/linear/src/initiatives.js +84 -0
  18. package/dist/plugins/linear/src/issues.js +267 -0
  19. package/dist/plugins/linear/src/labels.js +74 -0
  20. package/dist/plugins/linear/src/organization.js +13 -0
  21. package/dist/plugins/linear/src/projects.js +200 -0
  22. package/dist/plugins/linear/src/shared.js +234 -0
  23. package/dist/plugins/linear/src/states.js +41 -0
  24. package/dist/plugins/linear/src/teams.js +77 -0
  25. package/dist/plugins/linear/src/users.js +37 -0
  26. package/dist/plugins/linear/src/views.js +105 -0
  27. package/dist/plugins/linear/src/webhooks.js +61 -0
  28. package/dist/plugins/vercel/src/account.js +11 -0
  29. package/dist/plugins/vercel/src/deployments.js +79 -0
  30. package/dist/plugins/vercel/src/env.js +101 -0
  31. package/dist/plugins/vercel/src/index.js +27 -0
  32. package/dist/plugins/vercel/src/projects.js +29 -0
  33. package/dist/plugins/vercel/src/shared.js +73 -0
  34. package/package.json +3 -2
@@ -0,0 +1,84 @@
1
+ import * as t from "typebox";
2
+ import { INITIATIVE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
+ export function registerInitiativeActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("initiative.list", "List initiatives.", "initiatives", "InitiativeFilter", INITIATIVE_FIELDS);
7
+ getAction("initiative.get", "Get an initiative by ID or slug.", "initiative", INITIATIVE_FIELDS);
8
+ rl.registerAction("initiative.create", {
9
+ description: "Create an initiative. Status: Planned | Active | Completed.",
10
+ inputSchema: t.Object({
11
+ name: t.String({ description: "The name of the initiative" }),
12
+ description: t.Optional(t.String({ description: "The description of the initiative" })),
13
+ content: t.Optional(t.String({ description: "The initiative's content in markdown format" })),
14
+ icon: t.Optional(t.String({ description: "The initiative's icon" })),
15
+ color: t.Optional(t.String({ description: "The initiative's color (hex)" })),
16
+ ownerId: t.Optional(t.String({ description: "The owner of the initiative" })),
17
+ status: t.Optional(t.String({ description: "The initiative's status (InitiativeStatus: Planned | Active | Completed)" })),
18
+ targetDate: t.Optional(t.String({ description: "The estimated completion date of the initiative (TimelessDate, YYYY-MM-DD)" })),
19
+ targetDateResolution: t.Optional(t.String({ description: "The resolution of the initiative's estimated completion date (DateResolutionType)" })),
20
+ sortOrder: t.Optional(t.Number({ description: "The sort order of the initiative within the workspace (Float)" })),
21
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
22
+ }),
23
+ async execute(input, ctx) {
24
+ requireUnscoped(ctx, "initiatives.*");
25
+ const data = await gql(key(ctx), `mutation($input: InitiativeCreateInput!) { initiativeCreate(input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { input: input });
26
+ return data.initiativeCreate?.initiative;
27
+ },
28
+ });
29
+ rl.registerAction("initiative.update", {
30
+ description: "Update an initiative.",
31
+ inputSchema: t.Object({
32
+ id: t.String({ description: "The identifier of the initiative to update" }),
33
+ name: t.Optional(t.String({ description: "The name of the initiative" })),
34
+ description: t.Optional(t.String({ description: "The description of the initiative" })),
35
+ content: t.Optional(t.String({ description: "The initiative's content in markdown format" })),
36
+ icon: t.Optional(t.String({ description: "The initiative's icon" })),
37
+ color: t.Optional(t.String({ description: "The initiative's color (hex)" })),
38
+ ownerId: t.Optional(t.String({ description: "The owner of the initiative" })),
39
+ status: t.Optional(t.String({ description: "The initiative's status (InitiativeStatus: Planned | Active | Completed)" })),
40
+ targetDate: t.Optional(t.String({ description: "The estimated completion date (TimelessDate, YYYY-MM-DD). Set to null to clear" })),
41
+ targetDateResolution: t.Optional(t.String({ description: "The resolution of the initiative's estimated completion date (DateResolutionType)" })),
42
+ sortOrder: t.Optional(t.Number({ description: "The sort order of the initiative within the workspace (Float)" })),
43
+ trashed: t.Optional(t.Boolean({ description: "Whether the initiative has been trashed. Set to true to trash, or null to restore" })),
44
+ }),
45
+ async execute(input, ctx) {
46
+ requireUnscoped(ctx, "initiatives.*");
47
+ const { id, ...fields } = input;
48
+ const data = await gql(key(ctx), `mutation($id: String!, $input: InitiativeUpdateInput!) { initiativeUpdate(id: $id, input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { id, input: fields });
49
+ return data.initiativeUpdate?.initiative;
50
+ },
51
+ });
52
+ rl.registerAction("initiative.delete", {
53
+ description: "Trash an initiative.",
54
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the initiative to delete" }) }),
55
+ async execute(input, ctx) {
56
+ requireUnscoped(ctx, "initiatives.*");
57
+ const data = await gql(key(ctx), `mutation($id: String!) { initiativeDelete(id: $id) { success } }`, { id: input.id });
58
+ return data.initiativeDelete;
59
+ },
60
+ });
61
+ rl.registerAction("initiative.addProject", {
62
+ description: "Associate a project with an initiative. Use this action for project-to-initiative linking; project.update does not accept initiativeId. Verify with initiative.get or the returned initiative.projects list.",
63
+ inputSchema: t.Object({
64
+ initiativeId: t.String({ description: "The identifier of the initiative" }),
65
+ projectId: t.String({ description: "The identifier of the project" }),
66
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project within the initiative (Float)" })),
67
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
68
+ }),
69
+ async execute(input, ctx) {
70
+ requireUnscoped(ctx, "initiatives.*");
71
+ const data = await gql(key(ctx), `mutation($input: InitiativeToProjectCreateInput!) { initiativeToProjectCreate(input: $input) { success initiativeToProject { id initiative { id name projects { nodes { id name } } } project { id name } } } }`, { input: input });
72
+ return data.initiativeToProjectCreate;
73
+ },
74
+ });
75
+ rl.registerAction("initiative.removeProject", {
76
+ description: "Remove a project from an initiative. Pass the link id returned by initiative.addProject, then verify with initiative.get.",
77
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the initiativeToProject to delete" }) }),
78
+ async execute(input, ctx) {
79
+ requireUnscoped(ctx, "initiatives.*");
80
+ const data = await gql(key(ctx), `mutation($id: String!) { initiativeToProjectDelete(id: $id) { success } }`, { id: input.id });
81
+ return data.initiativeToProjectDelete;
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,267 @@
1
+ import * as t from "typebox";
2
+ import { COMMENT_FIELDS, ISSUE_FIELDS, ISSUE_LITE, LIST_INPUT_SCHEMA, assertIssueInScope, buildConnArgs, ensureScopeLabelsOnCreateOrReplace, forbidScopeLabelRemoval, gql, issueHasScope, key, mergeIssueScopeFilter, } from "./shared.js";
3
+ export function registerIssueActions(rl) {
4
+ rl.registerAction("issue.create", {
5
+ description: "Create an issue. teamId is required; title is required unless a template is applied.",
6
+ inputSchema: t.Object({
7
+ teamId: t.String({ description: "The identifier of the team associated with the issue" }),
8
+ title: t.String({ description: "The title of the issue" }),
9
+ description: t.Optional(t.String({ description: "The issue description in markdown format" })),
10
+ assigneeId: t.Optional(t.String({ description: "The identifier of the user to assign the issue to" })),
11
+ priority: t.Optional(t.Number({ description: "Priority. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" })),
12
+ stateId: t.Optional(t.String({ description: "The team workflow state of the issue" })),
13
+ labelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the issue labels associated with this ticket" })),
14
+ parentId: t.Optional(t.String({ description: "The identifier of the parent issue. UUID or issue identifier (e.g., 'LIN-123')" })),
15
+ projectId: t.Optional(t.String({ description: "The project associated with the issue" })),
16
+ projectMilestoneId: t.Optional(t.String({ description: "The project milestone associated with the issue" })),
17
+ cycleId: t.Optional(t.String({ description: "The cycle associated with the issue" })),
18
+ estimate: t.Optional(t.Number({ description: "The estimated complexity of the issue (Int)" })),
19
+ dueDate: t.Optional(t.String({ description: "The date at which the issue is due (TimelessDate, YYYY-MM-DD)" })),
20
+ subscriberIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the users subscribing to this ticket" })),
21
+ templateId: t.Optional(t.String({ description: "The identifier of a template the issue should be created from" })),
22
+ useDefaultTemplate: t.Optional(t.Boolean({ description: "Apply the team's default template based on the user's membership" })),
23
+ sortOrder: t.Optional(t.Number({ description: "The position of the issue related to other issues (Float)" })),
24
+ subIssueSortOrder: t.Optional(t.Number({ description: "The position of the issue in its parent's sub-issue list (Float)" })),
25
+ releaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the releases to associate with this issue" })),
26
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
27
+ }),
28
+ async execute(input, ctx) {
29
+ const fields = { ...input };
30
+ fields.labelIds = ensureScopeLabelsOnCreateOrReplace(ctx, fields.labelIds);
31
+ const data = await gql(key(ctx), `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { input: fields });
32
+ return data.issueCreate?.issue;
33
+ },
34
+ });
35
+ rl.registerAction("issue.get", {
36
+ description: "Get an issue by ID or identifier (e.g. 'THE-154')",
37
+ inputSchema: t.Object({ issueId: t.String() }),
38
+ async execute(input, ctx) {
39
+ const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { ${ISSUE_FIELDS} } }`, { id: input.issueId });
40
+ const issue = data.issue;
41
+ if (!issueHasScope(ctx, issue))
42
+ throw new Error("Linear issue is not available to this scoped connection");
43
+ return issue;
44
+ },
45
+ });
46
+ rl.registerAction("issue.list", {
47
+ description: "List issues. Pass `filter` for state/label/project/etc. Default hides archived.",
48
+ inputSchema: t.Object({
49
+ ...LIST_INPUT_SCHEMA,
50
+ teamId: t.Optional(t.String({ description: "Convenience: filter by team" })),
51
+ assigneeId: t.Optional(t.String({ description: "Convenience: filter by assignee" })),
52
+ }),
53
+ async execute(input, ctx) {
54
+ const opts = (input ?? {});
55
+ // Merge convenience filters into `filter`
56
+ const merged = { ...(opts.filter ?? {}) };
57
+ if (opts.teamId)
58
+ merged.team = { id: { eq: opts.teamId } };
59
+ if (opts.assigneeId)
60
+ merged.assignee = { id: { eq: opts.assigneeId } };
61
+ const filter = mergeIssueScopeFilter(ctx, Object.keys(merged).length > 0 ? merged : undefined);
62
+ const { argsDecl, argsCall, vars } = buildConnArgs({ ...opts, filter }, "IssueFilter");
63
+ const data = await gql(key(ctx), `query${argsDecl} { issues${argsCall} { nodes { ${ISSUE_LITE} } pageInfo { hasNextPage endCursor } } }`, vars);
64
+ const conn = data.issues;
65
+ return { nodes: conn.nodes, pageInfo: conn.pageInfo };
66
+ },
67
+ });
68
+ rl.registerAction("issue.update", {
69
+ description: "Update an issue. All fields optional; only provided fields are updated.",
70
+ inputSchema: t.Object({
71
+ issueId: t.String({ description: "The identifier of the issue to update" }),
72
+ title: t.Optional(t.String({ description: "The issue title" })),
73
+ description: t.Optional(t.String({ description: "The issue description in markdown format" })),
74
+ assigneeId: t.Optional(t.String({ description: "The identifier of the user to assign the issue to" })),
75
+ stateId: t.Optional(t.String({ description: "The team workflow state of the issue" })),
76
+ priority: t.Optional(t.Number({ description: "Priority. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" })),
77
+ labelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the issue labels associated with this ticket (replaces all)" })),
78
+ addedLabelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of issue labels to be added to this issue" })),
79
+ removedLabelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of issue labels to be removed from this issue" })),
80
+ projectId: t.Optional(t.String({ description: "The project associated with the issue" })),
81
+ projectMilestoneId: t.Optional(t.String({ description: "The project milestone associated with the issue" })),
82
+ cycleId: t.Optional(t.String({ description: "The cycle associated with the issue" })),
83
+ parentId: t.Optional(t.String({ description: "The identifier of the parent issue. UUID or issue identifier (e.g., 'LIN-123')" })),
84
+ teamId: t.Optional(t.String({ description: "The identifier of the team associated with the issue (move issue to a different team)" })),
85
+ estimate: t.Optional(t.Number({ description: "The estimated complexity of the issue (Int)" })),
86
+ dueDate: t.Optional(t.String({ description: "The date at which the issue is due (TimelessDate, YYYY-MM-DD)" })),
87
+ subscriberIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the users subscribing to this ticket" })),
88
+ sortOrder: t.Optional(t.Number({ description: "The position of the issue related to other issues (Float)" })),
89
+ subIssueSortOrder: t.Optional(t.Number({ description: "The position of the issue in its parent's sub-issue list (Float)" })),
90
+ snoozedUntilAt: t.Optional(t.String({ description: "The time until which the issue will be snoozed in Triage view (DateTime)" })),
91
+ releaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the releases associated with this issue (replaces all)" })),
92
+ addedReleaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of releases to be added to this issue" })),
93
+ removedReleaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of releases to be removed from this issue" })),
94
+ trashed: t.Optional(t.Boolean({ description: "Whether the issue has been trashed" })),
95
+ }),
96
+ async execute(input, ctx) {
97
+ const { issueId, ...fields } = input;
98
+ await assertIssueInScope(ctx, String(issueId));
99
+ if (fields.removedLabelIds)
100
+ forbidScopeLabelRemoval(ctx, fields.removedLabelIds);
101
+ if (fields.labelIds)
102
+ fields.labelIds = ensureScopeLabelsOnCreateOrReplace(ctx, fields.labelIds);
103
+ const data = await gql(key(ctx), `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { id: issueId, input: fields });
104
+ return data.issueUpdate?.issue;
105
+ },
106
+ });
107
+ rl.registerAction("issue.delete", {
108
+ description: "Trash (soft-delete) an issue. Pass permanentlyDelete=true to bypass 30d grace period (admin only).",
109
+ inputSchema: t.Object({
110
+ issueId: t.String(),
111
+ permanentlyDelete: t.Optional(t.Boolean()),
112
+ }),
113
+ async execute(input, ctx) {
114
+ const { issueId, permanentlyDelete } = input;
115
+ await assertIssueInScope(ctx, issueId);
116
+ const data = await gql(key(ctx), `mutation($id: String!, $perm: Boolean) { issueDelete(id: $id, permanentlyDelete: $perm) { success } }`, { id: issueId, perm: permanentlyDelete ?? null });
117
+ return data.issueDelete;
118
+ },
119
+ });
120
+ rl.registerAction("issue.archive", {
121
+ description: "Archive an issue.",
122
+ inputSchema: t.Object({
123
+ issueId: t.String(),
124
+ trash: t.Optional(t.Boolean()),
125
+ }),
126
+ async execute(input, ctx) {
127
+ const { issueId, trash } = input;
128
+ await assertIssueInScope(ctx, issueId);
129
+ const data = await gql(key(ctx), `mutation($id: String!, $trash: Boolean) { issueArchive(id: $id, trash: $trash) { success } }`, { id: issueId, trash: trash ?? null });
130
+ return data.issueArchive;
131
+ },
132
+ });
133
+ rl.registerAction("issue.unarchive", {
134
+ description: "Unarchive an issue.",
135
+ inputSchema: t.Object({ issueId: t.String() }),
136
+ async execute(input, ctx) {
137
+ const issueId = input.issueId;
138
+ await assertIssueInScope(ctx, issueId);
139
+ const data = await gql(key(ctx), `mutation($id: String!) { issueUnarchive(id: $id) { success } }`, { id: issueId });
140
+ return data.issueUnarchive;
141
+ },
142
+ });
143
+ rl.registerAction("issue.search", {
144
+ description: "Search issues by text query using full-text and vector search. Rate-limited to 30 req/min.",
145
+ inputSchema: t.Object({
146
+ term: t.String({ description: "Search string to look for" }),
147
+ limit: t.Optional(t.Number({ description: "Max results (forward pagination, default 50)" })),
148
+ filter: t.Optional(t.Object({}, { description: "Optional IssueFilter" })),
149
+ includeComments: t.Optional(t.Boolean({ description: "Should associated comments be searched (default false)" })),
150
+ includeArchived: t.Optional(t.Boolean({ description: "Should archived resources be included (default false)" })),
151
+ teamId: t.Optional(t.String({ description: "UUID of a team to boost in search results" })),
152
+ orderBy: t.Optional(t.String({ description: "PaginationOrderBy: createdAt | updatedAt" })),
153
+ after: t.Optional(t.String({ description: "Cursor for forward pagination" })),
154
+ before: t.Optional(t.String({ description: "Cursor for backward pagination" })),
155
+ }),
156
+ async execute(input, ctx) {
157
+ const opts = input;
158
+ const data = await gql(key(ctx), `query($term: String!, $first: Int, $filter: IssueFilter, $includeComments: Boolean, $includeArchived: Boolean, $teamId: String, $orderBy: PaginationOrderBy, $after: String, $before: String) {
159
+ searchIssues(term: $term, first: $first, filter: $filter, includeComments: $includeComments, includeArchived: $includeArchived, teamId: $teamId, orderBy: $orderBy, after: $after, before: $before) {
160
+ nodes { ${ISSUE_LITE} }
161
+ totalCount
162
+ pageInfo { hasNextPage endCursor }
163
+ }
164
+ }`, {
165
+ term: opts.term,
166
+ first: opts.limit ?? 50,
167
+ filter: mergeIssueScopeFilter(ctx, opts.filter) ?? null,
168
+ includeComments: opts.includeComments ?? null,
169
+ includeArchived: opts.includeArchived ?? null,
170
+ teamId: opts.teamId ?? null,
171
+ orderBy: opts.orderBy ?? null,
172
+ after: opts.after ?? null,
173
+ before: opts.before ?? null,
174
+ });
175
+ return data.searchIssues;
176
+ },
177
+ });
178
+ rl.registerAction("issue.addLabel", {
179
+ description: "Add a single label to an issue.",
180
+ inputSchema: t.Object({
181
+ issueId: t.String(),
182
+ labelId: t.String(),
183
+ }),
184
+ async execute(input, ctx) {
185
+ const { issueId, labelId } = input;
186
+ await assertIssueInScope(ctx, issueId);
187
+ const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueAddLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
188
+ return data.issueAddLabel;
189
+ },
190
+ });
191
+ rl.registerAction("issue.removeLabel", {
192
+ description: "Remove a single label from an issue.",
193
+ inputSchema: t.Object({
194
+ issueId: t.String(),
195
+ labelId: t.String(),
196
+ }),
197
+ async execute(input, ctx) {
198
+ const { issueId, labelId } = input;
199
+ await assertIssueInScope(ctx, issueId);
200
+ forbidScopeLabelRemoval(ctx, labelId);
201
+ const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueRemoveLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
202
+ return data.issueRemoveLabel;
203
+ },
204
+ });
205
+ rl.registerAction("issue.subscribe", {
206
+ description: "Subscribe a user to issue notifications (defaults to current user).",
207
+ inputSchema: t.Object({
208
+ issueId: t.String(),
209
+ userId: t.Optional(t.String()),
210
+ userEmail: t.Optional(t.String()),
211
+ }),
212
+ async execute(input, ctx) {
213
+ const { issueId, userId, userEmail } = input;
214
+ await assertIssueInScope(ctx, String(issueId));
215
+ const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
216
+ issueSubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
217
+ }`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
218
+ return data.issueSubscribe;
219
+ },
220
+ });
221
+ rl.registerAction("issue.unsubscribe", {
222
+ description: "Unsubscribe a user from issue notifications (defaults to current user).",
223
+ inputSchema: t.Object({
224
+ issueId: t.String(),
225
+ userId: t.Optional(t.String()),
226
+ userEmail: t.Optional(t.String()),
227
+ }),
228
+ async execute(input, ctx) {
229
+ const { issueId, userId, userEmail } = input;
230
+ await assertIssueInScope(ctx, String(issueId));
231
+ const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
232
+ issueUnsubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
233
+ }`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
234
+ return data.issueUnsubscribe;
235
+ },
236
+ });
237
+ rl.registerAction("issue.addLink", {
238
+ description: "Create a relation between two issues.",
239
+ inputSchema: t.Object({
240
+ issueId: t.String({ description: "The identifier of the issue that is related to another issue. UUID or issue identifier (e.g., 'LIN-123')" }),
241
+ relatedIssueId: t.String({ description: "The identifier of the related issue. UUID or issue identifier (e.g., 'LIN-123')" }),
242
+ type: t.String({ description: "IssueRelationType: blocks | duplicate | related | similar" }),
243
+ }),
244
+ async execute(input, ctx) {
245
+ const fields = input;
246
+ await assertIssueInScope(ctx, String(fields.issueId));
247
+ await assertIssueInScope(ctx, String(fields.relatedIssueId));
248
+ const data = await gql(key(ctx), `mutation($input: IssueRelationCreateInput!) { issueRelationCreate(input: $input) { success issueRelation { id type } } }`, { input: fields });
249
+ return data.issueRelationCreate;
250
+ },
251
+ });
252
+ rl.registerAction("issue.listComments", {
253
+ description: "List comments on an issue.",
254
+ inputSchema: t.Object({
255
+ issueId: t.String(),
256
+ limit: t.Optional(t.Number()),
257
+ }),
258
+ async execute(input, ctx) {
259
+ const { issueId, limit } = input;
260
+ await assertIssueInScope(ctx, issueId);
261
+ const data = await gql(key(ctx), `query($id: String!, $first: Int) {
262
+ issue(id: $id) { comments(first: $first) { nodes { ${COMMENT_FIELDS} } } }
263
+ }`, { id: issueId, first: limit ?? 50 });
264
+ return data.issue?.comments?.nodes;
265
+ },
266
+ });
267
+ }
@@ -0,0 +1,74 @@
1
+ import * as t from "typebox";
2
+ import { LABEL_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
+ export function registerLabelActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("label.list", "List labels (workspace + team-scoped).", "issueLabels", "IssueLabelFilter", LABEL_FIELDS);
7
+ getAction("label.get", "Get a label by ID.", "issueLabel", LABEL_FIELDS);
8
+ rl.registerAction("label.create", {
9
+ description: "Create a label. Omit teamId for a workspace-level label.",
10
+ inputSchema: t.Object({
11
+ name: t.String({ description: "The name of the label" }),
12
+ teamId: t.Optional(t.String({ description: "The team associated with the label. If omitted, the label is workspace-scoped" })),
13
+ color: t.Optional(t.String({ description: "The color of the label (hex)" })),
14
+ description: t.Optional(t.String({ description: "The description of the label" })),
15
+ parentId: t.Optional(t.String({ description: "The identifier of the parent label (group label)" })),
16
+ isGroup: t.Optional(t.Boolean({ description: "Whether the label is a group" })),
17
+ retiredAt: t.Optional(t.String({ description: "The time at which the label was retired (DateTime). Set to null to restore a retired label" })),
18
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
19
+ replaceTeamLabels: t.Optional(t.Boolean({ description: "Replace all team-specific labels with the same name with this newly created workspace label (default false)" })),
20
+ }),
21
+ async execute(input, ctx) {
22
+ requireUnscoped(ctx, "label.create");
23
+ const { replaceTeamLabels, ...fields } = input;
24
+ const data = await gql(key(ctx), `mutation($input: IssueLabelCreateInput!, $replaceTeamLabels: Boolean) { issueLabelCreate(input: $input, replaceTeamLabels: $replaceTeamLabels) { success issueLabel { ${LABEL_FIELDS} } } }`, { input: fields, replaceTeamLabels: replaceTeamLabels ?? null });
25
+ return data.issueLabelCreate?.issueLabel;
26
+ },
27
+ });
28
+ rl.registerAction("label.update", {
29
+ description: "Update a label.",
30
+ inputSchema: t.Object({
31
+ id: t.String({ description: "The identifier of the label to update" }),
32
+ name: t.Optional(t.String({ description: "The name of the label" })),
33
+ color: t.Optional(t.String({ description: "The color of the label (hex)" })),
34
+ description: t.Optional(t.String({ description: "The description of the label" })),
35
+ parentId: t.Optional(t.String({ description: "The identifier of the parent label" })),
36
+ isGroup: t.Optional(t.Boolean({ description: "Whether the label is a group" })),
37
+ retiredAt: t.Optional(t.String({ description: "The time at which the label was retired (DateTime). Set to null to restore a retired label" })),
38
+ replaceTeamLabels: t.Optional(t.Boolean({ description: "Replace all team-specific labels with the same name with this updated workspace label (default false)" })),
39
+ }),
40
+ async execute(input, ctx) {
41
+ requireUnscoped(ctx, "label.update");
42
+ const { id, replaceTeamLabels, ...fields } = input;
43
+ const data = await gql(key(ctx), `mutation($id: String!, $input: IssueLabelUpdateInput!, $replaceTeamLabels: Boolean) { issueLabelUpdate(id: $id, input: $input, replaceTeamLabels: $replaceTeamLabels) { success issueLabel { ${LABEL_FIELDS} } } }`, { id, input: fields, replaceTeamLabels: replaceTeamLabels ?? null });
44
+ return data.issueLabelUpdate?.issueLabel;
45
+ },
46
+ });
47
+ rl.registerAction("label.delete", {
48
+ description: "Delete a label.",
49
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to delete" }) }),
50
+ async execute(input, ctx) {
51
+ requireUnscoped(ctx, "label.delete");
52
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelDelete(id: $id) { success } }`, { id: input.id });
53
+ return data.issueLabelDelete;
54
+ },
55
+ });
56
+ rl.registerAction("label.retire", {
57
+ description: "Retire a label. Retired labels remain visible but cannot be applied to new issues.",
58
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to retire" }) }),
59
+ async execute(input, ctx) {
60
+ requireUnscoped(ctx, "label.retire");
61
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRetire(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
62
+ return data.issueLabelRetire?.issueLabel;
63
+ },
64
+ });
65
+ rl.registerAction("label.restore", {
66
+ description: "Restore a previously retired label.",
67
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to restore" }) }),
68
+ async execute(input, ctx) {
69
+ requireUnscoped(ctx, "label.restore");
70
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRestore(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
71
+ return data.issueLabelRestore?.issueLabel;
72
+ },
73
+ });
74
+ }
@@ -0,0 +1,13 @@
1
+ import * as t from "typebox";
2
+ import { ORG_FIELDS, gql, key, requireUnscoped } from "./shared.js";
3
+ export function registerOrganizationActions(rl) {
4
+ rl.registerAction("org.get", {
5
+ description: "Get the authenticated workspace.",
6
+ inputSchema: t.Object({}),
7
+ async execute(_input, ctx) {
8
+ requireUnscoped(ctx, "org.get");
9
+ const data = await gql(key(ctx), `query { organization { ${ORG_FIELDS} } }`);
10
+ return data.organization;
11
+ },
12
+ });
13
+ }