runline 0.8.0 → 0.8.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,79 @@
1
+ import * as t from "typebox";
2
+ import { INITIATIVE_FIELDS, bindGetAction, bindListAction, gql, key } 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
+ const data = await gql(key(ctx), `mutation($input: InitiativeCreateInput!) { initiativeCreate(input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { input: input });
25
+ return data.initiativeCreate?.initiative;
26
+ },
27
+ });
28
+ rl.registerAction("initiative.update", {
29
+ description: "Update an initiative.",
30
+ inputSchema: t.Object({
31
+ id: t.String({ description: "The identifier of the initiative to update" }),
32
+ name: t.Optional(t.String({ description: "The name of the initiative" })),
33
+ description: t.Optional(t.String({ description: "The description of the initiative" })),
34
+ content: t.Optional(t.String({ description: "The initiative's content in markdown format" })),
35
+ icon: t.Optional(t.String({ description: "The initiative's icon" })),
36
+ color: t.Optional(t.String({ description: "The initiative's color (hex)" })),
37
+ ownerId: t.Optional(t.String({ description: "The owner of the initiative" })),
38
+ status: t.Optional(t.String({ description: "The initiative's status (InitiativeStatus: Planned | Active | Completed)" })),
39
+ targetDate: t.Optional(t.String({ description: "The estimated completion date (TimelessDate, YYYY-MM-DD). Set to null to clear" })),
40
+ targetDateResolution: t.Optional(t.String({ description: "The resolution of the initiative's estimated completion date (DateResolutionType)" })),
41
+ sortOrder: t.Optional(t.Number({ description: "The sort order of the initiative within the workspace (Float)" })),
42
+ trashed: t.Optional(t.Boolean({ description: "Whether the initiative has been trashed. Set to true to trash, or null to restore" })),
43
+ }),
44
+ async execute(input, ctx) {
45
+ const { id, ...fields } = input;
46
+ const data = await gql(key(ctx), `mutation($id: String!, $input: InitiativeUpdateInput!) { initiativeUpdate(id: $id, input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { id, input: fields });
47
+ return data.initiativeUpdate?.initiative;
48
+ },
49
+ });
50
+ rl.registerAction("initiative.delete", {
51
+ description: "Trash an initiative.",
52
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the initiative to delete" }) }),
53
+ async execute(input, ctx) {
54
+ const data = await gql(key(ctx), `mutation($id: String!) { initiativeDelete(id: $id) { success } }`, { id: input.id });
55
+ return data.initiativeDelete;
56
+ },
57
+ });
58
+ rl.registerAction("initiative.addProject", {
59
+ description: "Associate a project with an initiative. A project can only appear once in an initiative hierarchy.",
60
+ inputSchema: t.Object({
61
+ initiativeId: t.String({ description: "The identifier of the initiative" }),
62
+ projectId: t.String({ description: "The identifier of the project" }),
63
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project within the initiative (Float)" })),
64
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
65
+ }),
66
+ async execute(input, ctx) {
67
+ const data = await gql(key(ctx), `mutation($input: InitiativeToProjectCreateInput!) { initiativeToProjectCreate(input: $input) { success initiativeToProject { id } } }`, { input: input });
68
+ return data.initiativeToProjectCreate;
69
+ },
70
+ });
71
+ rl.registerAction("initiative.removeProject", {
72
+ description: "Remove a project from an initiative. Pass the link id returned by initiative.addProject.",
73
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the initiativeToProject to delete" }) }),
74
+ async execute(input, ctx) {
75
+ const data = await gql(key(ctx), `mutation($id: String!) { initiativeToProjectDelete(id: $id) { success } }`, { id: input.id });
76
+ return data.initiativeToProjectDelete;
77
+ },
78
+ });
79
+ }
@@ -0,0 +1,245 @@
1
+ import * as t from "typebox";
2
+ import { COMMENT_FIELDS, ISSUE_FIELDS, ISSUE_LITE, LIST_INPUT_SCHEMA, buildConnArgs, gql, key, } 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
+ const data = await gql(key(ctx), `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { input: fields });
31
+ return data.issueCreate?.issue;
32
+ },
33
+ });
34
+ rl.registerAction("issue.get", {
35
+ description: "Get an issue by ID or identifier (e.g. 'THE-154')",
36
+ inputSchema: t.Object({ issueId: t.String() }),
37
+ async execute(input, ctx) {
38
+ const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { ${ISSUE_FIELDS} } }`, { id: input.issueId });
39
+ return data.issue;
40
+ },
41
+ });
42
+ rl.registerAction("issue.list", {
43
+ description: "List issues. Pass `filter` for state/label/project/etc. Default hides archived.",
44
+ inputSchema: t.Object({
45
+ ...LIST_INPUT_SCHEMA,
46
+ teamId: t.Optional(t.String({ description: "Convenience: filter by team" })),
47
+ assigneeId: t.Optional(t.String({ description: "Convenience: filter by assignee" })),
48
+ }),
49
+ async execute(input, ctx) {
50
+ const opts = (input ?? {});
51
+ // Merge convenience filters into `filter`
52
+ const merged = { ...(opts.filter ?? {}) };
53
+ if (opts.teamId)
54
+ merged.team = { id: { eq: opts.teamId } };
55
+ if (opts.assigneeId)
56
+ merged.assignee = { id: { eq: opts.assigneeId } };
57
+ const filter = Object.keys(merged).length > 0 ? merged : undefined;
58
+ const { argsDecl, argsCall, vars } = buildConnArgs({ ...opts, filter }, "IssueFilter");
59
+ const data = await gql(key(ctx), `query${argsDecl} { issues${argsCall} { nodes { ${ISSUE_LITE} } pageInfo { hasNextPage endCursor } } }`, vars);
60
+ const conn = data.issues;
61
+ return { nodes: conn.nodes, pageInfo: conn.pageInfo };
62
+ },
63
+ });
64
+ rl.registerAction("issue.update", {
65
+ description: "Update an issue. All fields optional; only provided fields are updated.",
66
+ inputSchema: t.Object({
67
+ issueId: t.String({ description: "The identifier of the issue to update" }),
68
+ title: t.Optional(t.String({ description: "The issue title" })),
69
+ description: t.Optional(t.String({ description: "The issue description in markdown format" })),
70
+ assigneeId: t.Optional(t.String({ description: "The identifier of the user to assign the issue to" })),
71
+ stateId: t.Optional(t.String({ description: "The team workflow state of the issue" })),
72
+ priority: t.Optional(t.Number({ description: "Priority. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" })),
73
+ labelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the issue labels associated with this ticket (replaces all)" })),
74
+ addedLabelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of issue labels to be added to this issue" })),
75
+ removedLabelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of issue labels to be removed from this issue" })),
76
+ projectId: t.Optional(t.String({ description: "The project associated with the issue" })),
77
+ projectMilestoneId: t.Optional(t.String({ description: "The project milestone associated with the issue" })),
78
+ cycleId: t.Optional(t.String({ description: "The cycle associated with the issue" })),
79
+ parentId: t.Optional(t.String({ description: "The identifier of the parent issue. UUID or issue identifier (e.g., 'LIN-123')" })),
80
+ teamId: t.Optional(t.String({ description: "The identifier of the team associated with the issue (move issue to a different team)" })),
81
+ estimate: t.Optional(t.Number({ description: "The estimated complexity of the issue (Int)" })),
82
+ dueDate: t.Optional(t.String({ description: "The date at which the issue is due (TimelessDate, YYYY-MM-DD)" })),
83
+ subscriberIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the users subscribing to this ticket" })),
84
+ sortOrder: t.Optional(t.Number({ description: "The position of the issue related to other issues (Float)" })),
85
+ subIssueSortOrder: t.Optional(t.Number({ description: "The position of the issue in its parent's sub-issue list (Float)" })),
86
+ snoozedUntilAt: t.Optional(t.String({ description: "The time until which the issue will be snoozed in Triage view (DateTime)" })),
87
+ releaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the releases associated with this issue (replaces all)" })),
88
+ addedReleaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of releases to be added to this issue" })),
89
+ removedReleaseIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of releases to be removed from this issue" })),
90
+ trashed: t.Optional(t.Boolean({ description: "Whether the issue has been trashed" })),
91
+ }),
92
+ async execute(input, ctx) {
93
+ const { issueId, ...fields } = input;
94
+ const data = await gql(key(ctx), `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { id: issueId, input: fields });
95
+ return data.issueUpdate?.issue;
96
+ },
97
+ });
98
+ rl.registerAction("issue.delete", {
99
+ description: "Trash (soft-delete) an issue. Pass permanentlyDelete=true to bypass 30d grace period (admin only).",
100
+ inputSchema: t.Object({
101
+ issueId: t.String(),
102
+ permanentlyDelete: t.Optional(t.Boolean()),
103
+ }),
104
+ async execute(input, ctx) {
105
+ const { issueId, permanentlyDelete } = input;
106
+ const data = await gql(key(ctx), `mutation($id: String!, $perm: Boolean) { issueDelete(id: $id, permanentlyDelete: $perm) { success } }`, { id: issueId, perm: permanentlyDelete ?? null });
107
+ return data.issueDelete;
108
+ },
109
+ });
110
+ rl.registerAction("issue.archive", {
111
+ description: "Archive an issue.",
112
+ inputSchema: t.Object({
113
+ issueId: t.String(),
114
+ trash: t.Optional(t.Boolean()),
115
+ }),
116
+ async execute(input, ctx) {
117
+ const { issueId, trash } = input;
118
+ const data = await gql(key(ctx), `mutation($id: String!, $trash: Boolean) { issueArchive(id: $id, trash: $trash) { success } }`, { id: issueId, trash: trash ?? null });
119
+ return data.issueArchive;
120
+ },
121
+ });
122
+ rl.registerAction("issue.unarchive", {
123
+ description: "Unarchive an issue.",
124
+ inputSchema: t.Object({ issueId: t.String() }),
125
+ async execute(input, ctx) {
126
+ const data = await gql(key(ctx), `mutation($id: String!) { issueUnarchive(id: $id) { success } }`, { id: input.issueId });
127
+ return data.issueUnarchive;
128
+ },
129
+ });
130
+ rl.registerAction("issue.search", {
131
+ description: "Search issues by text query using full-text and vector search. Rate-limited to 30 req/min.",
132
+ inputSchema: t.Object({
133
+ term: t.String({ description: "Search string to look for" }),
134
+ limit: t.Optional(t.Number({ description: "Max results (forward pagination, default 50)" })),
135
+ filter: t.Optional(t.Object({}, { description: "Optional IssueFilter" })),
136
+ includeComments: t.Optional(t.Boolean({ description: "Should associated comments be searched (default false)" })),
137
+ includeArchived: t.Optional(t.Boolean({ description: "Should archived resources be included (default false)" })),
138
+ teamId: t.Optional(t.String({ description: "UUID of a team to boost in search results" })),
139
+ orderBy: t.Optional(t.String({ description: "PaginationOrderBy: createdAt | updatedAt" })),
140
+ after: t.Optional(t.String({ description: "Cursor for forward pagination" })),
141
+ before: t.Optional(t.String({ description: "Cursor for backward pagination" })),
142
+ }),
143
+ async execute(input, ctx) {
144
+ const opts = input;
145
+ 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) {
146
+ searchIssues(term: $term, first: $first, filter: $filter, includeComments: $includeComments, includeArchived: $includeArchived, teamId: $teamId, orderBy: $orderBy, after: $after, before: $before) {
147
+ nodes { ${ISSUE_LITE} }
148
+ totalCount
149
+ pageInfo { hasNextPage endCursor }
150
+ }
151
+ }`, {
152
+ term: opts.term,
153
+ first: opts.limit ?? 50,
154
+ filter: opts.filter ?? null,
155
+ includeComments: opts.includeComments ?? null,
156
+ includeArchived: opts.includeArchived ?? null,
157
+ teamId: opts.teamId ?? null,
158
+ orderBy: opts.orderBy ?? null,
159
+ after: opts.after ?? null,
160
+ before: opts.before ?? null,
161
+ });
162
+ return data.searchIssues;
163
+ },
164
+ });
165
+ rl.registerAction("issue.addLabel", {
166
+ description: "Add a single label to an issue.",
167
+ inputSchema: t.Object({
168
+ issueId: t.String(),
169
+ labelId: t.String(),
170
+ }),
171
+ async execute(input, ctx) {
172
+ const { issueId, labelId } = input;
173
+ const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueAddLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
174
+ return data.issueAddLabel;
175
+ },
176
+ });
177
+ rl.registerAction("issue.removeLabel", {
178
+ description: "Remove a single label from an issue.",
179
+ inputSchema: t.Object({
180
+ issueId: t.String(),
181
+ labelId: t.String(),
182
+ }),
183
+ async execute(input, ctx) {
184
+ const { issueId, labelId } = input;
185
+ const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueRemoveLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
186
+ return data.issueRemoveLabel;
187
+ },
188
+ });
189
+ rl.registerAction("issue.subscribe", {
190
+ description: "Subscribe a user to issue notifications (defaults to current user).",
191
+ inputSchema: t.Object({
192
+ issueId: t.String(),
193
+ userId: t.Optional(t.String()),
194
+ userEmail: t.Optional(t.String()),
195
+ }),
196
+ async execute(input, ctx) {
197
+ const { issueId, userId, userEmail } = input;
198
+ const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
199
+ issueSubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
200
+ }`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
201
+ return data.issueSubscribe;
202
+ },
203
+ });
204
+ rl.registerAction("issue.unsubscribe", {
205
+ description: "Unsubscribe a user from issue notifications (defaults to current user).",
206
+ inputSchema: t.Object({
207
+ issueId: t.String(),
208
+ userId: t.Optional(t.String()),
209
+ userEmail: t.Optional(t.String()),
210
+ }),
211
+ async execute(input, ctx) {
212
+ const { issueId, userId, userEmail } = input;
213
+ const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
214
+ issueUnsubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
215
+ }`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
216
+ return data.issueUnsubscribe;
217
+ },
218
+ });
219
+ rl.registerAction("issue.addLink", {
220
+ description: "Create a relation between two issues.",
221
+ inputSchema: t.Object({
222
+ issueId: t.String({ description: "The identifier of the issue that is related to another issue. UUID or issue identifier (e.g., 'LIN-123')" }),
223
+ relatedIssueId: t.String({ description: "The identifier of the related issue. UUID or issue identifier (e.g., 'LIN-123')" }),
224
+ type: t.String({ description: "IssueRelationType: blocks | duplicate | related | similar" }),
225
+ }),
226
+ async execute(input, ctx) {
227
+ const data = await gql(key(ctx), `mutation($input: IssueRelationCreateInput!) { issueRelationCreate(input: $input) { success issueRelation { id type } } }`, { input: input });
228
+ return data.issueRelationCreate;
229
+ },
230
+ });
231
+ rl.registerAction("issue.listComments", {
232
+ description: "List comments on an issue.",
233
+ inputSchema: t.Object({
234
+ issueId: t.String(),
235
+ limit: t.Optional(t.Number()),
236
+ }),
237
+ async execute(input, ctx) {
238
+ const { issueId, limit } = input;
239
+ const data = await gql(key(ctx), `query($id: String!, $first: Int) {
240
+ issue(id: $id) { comments(first: $first) { nodes { ${COMMENT_FIELDS} } } }
241
+ }`, { id: issueId, first: limit ?? 50 });
242
+ return data.issue?.comments?.nodes;
243
+ },
244
+ });
245
+ }
@@ -0,0 +1,69 @@
1
+ import * as t from "typebox";
2
+ import { LABEL_FIELDS, bindGetAction, bindListAction, gql, key } 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
+ const { replaceTeamLabels, ...fields } = input;
23
+ 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 });
24
+ return data.issueLabelCreate?.issueLabel;
25
+ },
26
+ });
27
+ rl.registerAction("label.update", {
28
+ description: "Update a label.",
29
+ inputSchema: t.Object({
30
+ id: t.String({ description: "The identifier of the label to update" }),
31
+ name: t.Optional(t.String({ description: "The name of the label" })),
32
+ color: t.Optional(t.String({ description: "The color of the label (hex)" })),
33
+ description: t.Optional(t.String({ description: "The description of the label" })),
34
+ parentId: t.Optional(t.String({ description: "The identifier of the parent label" })),
35
+ isGroup: t.Optional(t.Boolean({ description: "Whether the label is a group" })),
36
+ retiredAt: t.Optional(t.String({ description: "The time at which the label was retired (DateTime). Set to null to restore a retired label" })),
37
+ replaceTeamLabels: t.Optional(t.Boolean({ description: "Replace all team-specific labels with the same name with this updated workspace label (default false)" })),
38
+ }),
39
+ async execute(input, ctx) {
40
+ const { id, replaceTeamLabels, ...fields } = input;
41
+ 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 });
42
+ return data.issueLabelUpdate?.issueLabel;
43
+ },
44
+ });
45
+ rl.registerAction("label.delete", {
46
+ description: "Delete a label.",
47
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to delete" }) }),
48
+ async execute(input, ctx) {
49
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelDelete(id: $id) { success } }`, { id: input.id });
50
+ return data.issueLabelDelete;
51
+ },
52
+ });
53
+ rl.registerAction("label.retire", {
54
+ description: "Retire a label. Retired labels remain visible but cannot be applied to new issues.",
55
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to retire" }) }),
56
+ async execute(input, ctx) {
57
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRetire(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
58
+ return data.issueLabelRetire?.issueLabel;
59
+ },
60
+ });
61
+ rl.registerAction("label.restore", {
62
+ description: "Restore a previously retired label.",
63
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the label to restore" }) }),
64
+ async execute(input, ctx) {
65
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRestore(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
66
+ return data.issueLabelRestore?.issueLabel;
67
+ },
68
+ });
69
+ }
@@ -0,0 +1,12 @@
1
+ import * as t from "typebox";
2
+ import { ORG_FIELDS, gql, key } 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
+ const data = await gql(key(ctx), `query { organization { ${ORG_FIELDS} } }`);
9
+ return data.organization;
10
+ },
11
+ });
12
+ }
@@ -0,0 +1,189 @@
1
+ import * as t from "typebox";
2
+ import { MILESTONE_FIELDS, PROJECT_FIELDS, PROJECT_UPDATE_FIELDS, bindGetAction, bindListAction, gql, key, } from "./shared.js";
3
+ export function registerProjectActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("project.list", "List projects.", "projects", "ProjectFilter", PROJECT_FIELDS);
7
+ getAction("project.get", "Get a project by ID or slug.", "project", PROJECT_FIELDS);
8
+ rl.registerAction("project.create", {
9
+ description: "Create a project. teamIds is required.",
10
+ inputSchema: t.Object({
11
+ name: t.String({ description: "The name of the project" }),
12
+ teamIds: t.Array(t.Unknown(), { description: "The identifiers of the teams this project is associated with" }),
13
+ description: t.Optional(t.String({ description: "The description for the project" })),
14
+ content: t.Optional(t.String({ description: "The project content as markdown" })),
15
+ icon: t.Optional(t.String({ description: "The icon of the project" })),
16
+ color: t.Optional(t.String({ description: "The color of the project (hex)" })),
17
+ priority: t.Optional(t.Number({ description: "The priority of the project. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" })),
18
+ leadId: t.Optional(t.String({ description: "The identifier of the project lead" })),
19
+ memberIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the members of this project" })),
20
+ startDate: t.Optional(t.String({ description: "The planned start date of the project (TimelessDate, YYYY-MM-DD)" })),
21
+ startDateResolution: t.Optional(t.String({ description: "The resolution of the project's start date (DateResolutionType)" })),
22
+ targetDate: t.Optional(t.String({ description: "The planned target date of the project (TimelessDate, YYYY-MM-DD)" })),
23
+ targetDateResolution: t.Optional(t.String({ description: "The resolution of the project's estimated completion date (DateResolutionType)" })),
24
+ statusId: t.Optional(t.String({ description: "The ID of the project status" })),
25
+ labelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the project labels associated with this project" })),
26
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project in shared views (Float)" })),
27
+ templateId: t.Optional(t.String({ description: "The ID of a project template to apply when creating the project" })),
28
+ useDefaultTemplate: t.Optional(t.Boolean({ description: "Apply the default project template of the first team provided. Ignored if templateId is set" })),
29
+ convertedFromIssueId: t.Optional(t.String({ description: "The ID of the issue that was converted into this project" })),
30
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
31
+ slackChannelName: t.Optional(t.String({ description: "The full name for the Slack channel to create (including prefix). Creates and connects a Slack channel if provided" })),
32
+ }),
33
+ async execute(input, ctx) {
34
+ const { slackChannelName, ...fields } = input;
35
+ const data = await gql(key(ctx), `mutation($input: ProjectCreateInput!, $slackChannelName: String) { projectCreate(input: $input, slackChannelName: $slackChannelName) { success project { ${PROJECT_FIELDS} } } }`, { input: fields, slackChannelName: slackChannelName ?? null });
36
+ return data.projectCreate?.project;
37
+ },
38
+ });
39
+ rl.registerAction("project.update", {
40
+ description: "Update a project.",
41
+ inputSchema: t.Object({
42
+ id: t.String({ description: "The identifier of the project to update (UUID or slug)" }),
43
+ name: t.Optional(t.String({ description: "The name of the project" })),
44
+ description: t.Optional(t.String({ description: "The description for the project" })),
45
+ content: t.Optional(t.String({ description: "The project content as markdown" })),
46
+ icon: t.Optional(t.String({ description: "The icon of the project" })),
47
+ color: t.Optional(t.String({ description: "The color of the project (hex)" })),
48
+ priority: t.Optional(t.Number({ description: "The priority of the project. 0=No, 1=Urgent, 2=High, 3=Medium, 4=Low" })),
49
+ leadId: t.Optional(t.String({ description: "The identifier of the project lead" })),
50
+ memberIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the members of this project" })),
51
+ startDate: t.Optional(t.String({ description: "The planned start date (TimelessDate, YYYY-MM-DD)" })),
52
+ startDateResolution: t.Optional(t.String({ description: "The resolution of the project's start date (DateResolutionType)" })),
53
+ targetDate: t.Optional(t.String({ description: "The planned target date (TimelessDate, YYYY-MM-DD)" })),
54
+ targetDateResolution: t.Optional(t.String({ description: "The resolution of the project's estimated completion date (DateResolutionType)" })),
55
+ statusId: t.Optional(t.String({ description: "The ID of the project status" })),
56
+ labelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the project labels associated with this project" })),
57
+ teamIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the teams this project is associated with" })),
58
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project in shared views (Float)" })),
59
+ completedAt: t.Optional(t.String({ description: "The time at which the project was completed (DateTime)" })),
60
+ canceledAt: t.Optional(t.String({ description: "The time at which the project was canceled (DateTime)" })),
61
+ trashed: t.Optional(t.Boolean({ description: "Whether the project has been trashed. Set to true to trash, or null to restore" })),
62
+ }),
63
+ async execute(input, ctx) {
64
+ const { id, ...fields } = input;
65
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateInput!) { projectUpdate(id: $id, input: $input) { success project { ${PROJECT_FIELDS} } } }`, { id, input: fields });
66
+ return data.projectUpdate?.project;
67
+ },
68
+ });
69
+ rl.registerAction("project.delete", {
70
+ description: "Trash (soft-delete) a project. Restorable via project.unarchive.",
71
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project to delete" }), }),
72
+ async execute(input, ctx) {
73
+ const data = await gql(key(ctx), `mutation($id: String!) { projectDelete(id: $id) { success } }`, { id: input.id });
74
+ return data.projectDelete;
75
+ },
76
+ });
77
+ rl.registerAction("project.unarchive", {
78
+ description: "Restore a previously trashed or archived project.",
79
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project to restore (UUID or slug)" }), }),
80
+ async execute(input, ctx) {
81
+ const data = await gql(key(ctx), `mutation($id: String!) { projectUnarchive(id: $id) { success } }`, { id: input.id });
82
+ return data.projectUnarchive;
83
+ },
84
+ });
85
+ rl.registerAction("project.search", {
86
+ description: "Search projects by text. Rate-limited to 30 req/min.",
87
+ inputSchema: t.Object({
88
+ term: t.String({ description: "Search string to look for" }),
89
+ limit: t.Optional(t.Number({ description: "Max results (forward pagination, default 50)" })),
90
+ includeComments: t.Optional(t.Boolean({ description: "Should associated comments be searched (default false)" })),
91
+ teamId: t.Optional(t.String({ description: "UUID of a team to boost in search results" })),
92
+ }),
93
+ async execute(input, ctx) {
94
+ const opts = input;
95
+ const data = await gql(key(ctx), `query($term: String!, $first: Int, $includeComments: Boolean, $teamId: String) {
96
+ searchProjects(term: $term, first: $first, includeComments: $includeComments, teamId: $teamId) {
97
+ nodes { ${PROJECT_FIELDS} }
98
+ totalCount
99
+ }
100
+ }`, {
101
+ term: opts.term,
102
+ first: opts.limit ?? 50,
103
+ includeComments: opts.includeComments ?? null,
104
+ teamId: opts.teamId ?? null,
105
+ });
106
+ return data.searchProjects;
107
+ },
108
+ });
109
+ // Project milestones
110
+ listAction("milestone.list", "List project milestones.", "projectMilestones", "ProjectMilestoneFilter", MILESTONE_FIELDS);
111
+ getAction("milestone.get", "Get a project milestone by ID.", "projectMilestone", MILESTONE_FIELDS);
112
+ rl.registerAction("milestone.create", {
113
+ description: "Create a project milestone.",
114
+ inputSchema: t.Object({
115
+ projectId: t.String({ description: "Related project for the project milestone" }),
116
+ name: t.String({ description: "The name of the project milestone" }),
117
+ description: t.Optional(t.String({ description: "The description of the project milestone in markdown format" })),
118
+ targetDate: t.Optional(t.String({ description: "The planned target date of the project milestone (TimelessDate, YYYY-MM-DD)" })),
119
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project milestone within a project (Float)" })),
120
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
121
+ }),
122
+ async execute(input, ctx) {
123
+ const data = await gql(key(ctx), `mutation($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { input: input });
124
+ return data.projectMilestoneCreate?.projectMilestone;
125
+ },
126
+ });
127
+ rl.registerAction("milestone.update", {
128
+ description: "Update a project milestone.",
129
+ inputSchema: t.Object({
130
+ id: t.String({ description: "The identifier of the project milestone to update" }),
131
+ name: t.Optional(t.String({ description: "The name of the project milestone" })),
132
+ description: t.Optional(t.String({ description: "The description of the project milestone in markdown format" })),
133
+ targetDate: t.Optional(t.String({ description: "The planned target date (TimelessDate, YYYY-MM-DD)" })),
134
+ projectId: t.Optional(t.String({ description: "Related project for the project milestone (move to another project)" })),
135
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project milestone within a project (Float)" })),
136
+ }),
137
+ async execute(input, ctx) {
138
+ const { id, ...fields } = input;
139
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectMilestoneUpdateInput!) { projectMilestoneUpdate(id: $id, input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { id, input: fields });
140
+ return data.projectMilestoneUpdate?.projectMilestone;
141
+ },
142
+ });
143
+ rl.registerAction("milestone.delete", {
144
+ description: "Delete a project milestone.",
145
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project milestone to delete" }), }),
146
+ async execute(input, ctx) {
147
+ const data = await gql(key(ctx), `mutation($id: String!) { projectMilestoneDelete(id: $id) { success } }`, { id: input.id });
148
+ return data.projectMilestoneDelete;
149
+ },
150
+ });
151
+ // Project updates
152
+ listAction("projectUpdate.list", "List project updates.", "projectUpdates", "ProjectUpdateFilter", PROJECT_UPDATE_FIELDS);
153
+ rl.registerAction("projectUpdate.create", {
154
+ description: "Post a status update on a project.",
155
+ inputSchema: t.Object({
156
+ projectId: t.String({ description: "The project to associate the project update with" }),
157
+ body: t.Optional(t.String({ description: "The content of the project update in markdown format" })),
158
+ health: t.Optional(t.String({ description: "The health of the project at the time of the update (ProjectUpdateHealthType: onTrack | atRisk | offTrack)" })),
159
+ isDiffHidden: t.Optional(t.Boolean({ description: "Whether the diff between the current update and the previous one should be hidden" })),
160
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
161
+ }),
162
+ async execute(input, ctx) {
163
+ const data = await gql(key(ctx), `mutation($input: ProjectUpdateCreateInput!) { projectUpdateCreate(input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { input: input });
164
+ return data.projectUpdateCreate?.projectUpdate;
165
+ },
166
+ });
167
+ rl.registerAction("projectUpdate.update", {
168
+ description: "Update a project status update.",
169
+ inputSchema: t.Object({
170
+ id: t.String({ description: "The identifier of the project update to update" }),
171
+ body: t.Optional(t.String({ description: "The content of the project update in markdown format" })),
172
+ health: t.Optional(t.String({ description: "The health of the project at the time of the update (ProjectUpdateHealthType: onTrack | atRisk | offTrack)" })),
173
+ isDiffHidden: t.Optional(t.Boolean({ description: "Whether the diff between the current update and the previous one should be hidden" })),
174
+ }),
175
+ async execute(input, ctx) {
176
+ const { id, ...fields } = input;
177
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateUpdateInput!) { projectUpdateUpdate(id: $id, input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { id, input: fields });
178
+ return data.projectUpdateUpdate?.projectUpdate;
179
+ },
180
+ });
181
+ rl.registerAction("projectUpdate.archive", {
182
+ description: "Archive a project status update.",
183
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project update to archive" }), }),
184
+ async execute(input, ctx) {
185
+ const data = await gql(key(ctx), `mutation($id: String!) { projectUpdateArchive(id: $id) { success } }`, { id: input.id });
186
+ return data.projectUpdateArchive;
187
+ },
188
+ });
189
+ }