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,200 @@
1
+ import * as t from "typebox";
2
+ import { MILESTONE_FIELDS, PROJECT_FIELDS, PROJECT_UPDATE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped, } 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
+ requireUnscoped(ctx, "projects.*");
35
+ const { slackChannelName, ...fields } = input;
36
+ 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 });
37
+ return data.projectCreate?.project;
38
+ },
39
+ });
40
+ rl.registerAction("project.update", {
41
+ description: "Update a project.",
42
+ inputSchema: t.Object({
43
+ id: t.String({ description: "The identifier of the project to update (UUID or slug)" }),
44
+ name: t.Optional(t.String({ description: "The name of the project" })),
45
+ description: t.Optional(t.String({ description: "The description for the project" })),
46
+ content: t.Optional(t.String({ description: "The project content as markdown" })),
47
+ icon: t.Optional(t.String({ description: "The icon of the project" })),
48
+ color: t.Optional(t.String({ description: "The color of the project (hex)" })),
49
+ priority: t.Optional(t.Number({ description: "The priority of the project. 0=No, 1=Urgent, 2=High, 3=Medium, 4=Low" })),
50
+ leadId: t.Optional(t.String({ description: "The identifier of the project lead" })),
51
+ memberIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the members of this project" })),
52
+ startDate: t.Optional(t.String({ description: "The planned start date (TimelessDate, YYYY-MM-DD)" })),
53
+ startDateResolution: t.Optional(t.String({ description: "The resolution of the project's start date (DateResolutionType)" })),
54
+ targetDate: t.Optional(t.String({ description: "The planned target date (TimelessDate, YYYY-MM-DD)" })),
55
+ targetDateResolution: t.Optional(t.String({ description: "The resolution of the project's estimated completion date (DateResolutionType)" })),
56
+ statusId: t.Optional(t.String({ description: "The ID of the project status" })),
57
+ labelIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the project labels associated with this project" })),
58
+ teamIds: t.Optional(t.Array(t.Unknown(), { description: "The identifiers of the teams this project is associated with" })),
59
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project in shared views (Float)" })),
60
+ completedAt: t.Optional(t.String({ description: "The time at which the project was completed (DateTime)" })),
61
+ canceledAt: t.Optional(t.String({ description: "The time at which the project was canceled (DateTime)" })),
62
+ trashed: t.Optional(t.Boolean({ description: "Whether the project has been trashed. Set to true to trash, or null to restore" })),
63
+ }),
64
+ async execute(input, ctx) {
65
+ requireUnscoped(ctx, "projects.*");
66
+ const { id, ...fields } = input;
67
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateInput!) { projectUpdate(id: $id, input: $input) { success project { ${PROJECT_FIELDS} } } }`, { id, input: fields });
68
+ return data.projectUpdate?.project;
69
+ },
70
+ });
71
+ rl.registerAction("project.delete", {
72
+ description: "Trash (soft-delete) a project. Restorable via project.unarchive.",
73
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project to delete" }), }),
74
+ async execute(input, ctx) {
75
+ requireUnscoped(ctx, "projects.*");
76
+ const data = await gql(key(ctx), `mutation($id: String!) { projectDelete(id: $id) { success } }`, { id: input.id });
77
+ return data.projectDelete;
78
+ },
79
+ });
80
+ rl.registerAction("project.unarchive", {
81
+ description: "Restore a previously trashed or archived project.",
82
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project to restore (UUID or slug)" }), }),
83
+ async execute(input, ctx) {
84
+ requireUnscoped(ctx, "projects.*");
85
+ const data = await gql(key(ctx), `mutation($id: String!) { projectUnarchive(id: $id) { success } }`, { id: input.id });
86
+ return data.projectUnarchive;
87
+ },
88
+ });
89
+ rl.registerAction("project.search", {
90
+ description: "Search projects by text. Rate-limited to 30 req/min.",
91
+ inputSchema: t.Object({
92
+ term: t.String({ description: "Search string to look for" }),
93
+ limit: t.Optional(t.Number({ description: "Max results (forward pagination, default 50)" })),
94
+ includeComments: t.Optional(t.Boolean({ description: "Should associated comments be searched (default false)" })),
95
+ teamId: t.Optional(t.String({ description: "UUID of a team to boost in search results" })),
96
+ }),
97
+ async execute(input, ctx) {
98
+ requireUnscoped(ctx, "projects.*");
99
+ const opts = input;
100
+ const data = await gql(key(ctx), `query($term: String!, $first: Int, $includeComments: Boolean, $teamId: String) {
101
+ searchProjects(term: $term, first: $first, includeComments: $includeComments, teamId: $teamId) {
102
+ nodes { ${PROJECT_FIELDS} }
103
+ totalCount
104
+ }
105
+ }`, {
106
+ term: opts.term,
107
+ first: opts.limit ?? 50,
108
+ includeComments: opts.includeComments ?? null,
109
+ teamId: opts.teamId ?? null,
110
+ });
111
+ return data.searchProjects;
112
+ },
113
+ });
114
+ // Project milestones
115
+ listAction("milestone.list", "List project milestones.", "projectMilestones", "ProjectMilestoneFilter", MILESTONE_FIELDS);
116
+ getAction("milestone.get", "Get a project milestone by ID.", "projectMilestone", MILESTONE_FIELDS);
117
+ rl.registerAction("milestone.create", {
118
+ description: "Create a project milestone.",
119
+ inputSchema: t.Object({
120
+ projectId: t.String({ description: "Related project for the project milestone" }),
121
+ name: t.String({ description: "The name of the project milestone" }),
122
+ description: t.Optional(t.String({ description: "The description of the project milestone in markdown format" })),
123
+ targetDate: t.Optional(t.String({ description: "The planned target date of the project milestone (TimelessDate, YYYY-MM-DD)" })),
124
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project milestone within a project (Float)" })),
125
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
126
+ }),
127
+ async execute(input, ctx) {
128
+ requireUnscoped(ctx, "projects.*");
129
+ const data = await gql(key(ctx), `mutation($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { input: input });
130
+ return data.projectMilestoneCreate?.projectMilestone;
131
+ },
132
+ });
133
+ rl.registerAction("milestone.update", {
134
+ description: "Update a project milestone.",
135
+ inputSchema: t.Object({
136
+ id: t.String({ description: "The identifier of the project milestone to update" }),
137
+ name: t.Optional(t.String({ description: "The name of the project milestone" })),
138
+ description: t.Optional(t.String({ description: "The description of the project milestone in markdown format" })),
139
+ targetDate: t.Optional(t.String({ description: "The planned target date (TimelessDate, YYYY-MM-DD)" })),
140
+ projectId: t.Optional(t.String({ description: "Related project for the project milestone (move to another project)" })),
141
+ sortOrder: t.Optional(t.Number({ description: "The sort order for the project milestone within a project (Float)" })),
142
+ }),
143
+ async execute(input, ctx) {
144
+ requireUnscoped(ctx, "projects.*");
145
+ const { id, ...fields } = input;
146
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectMilestoneUpdateInput!) { projectMilestoneUpdate(id: $id, input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { id, input: fields });
147
+ return data.projectMilestoneUpdate?.projectMilestone;
148
+ },
149
+ });
150
+ rl.registerAction("milestone.delete", {
151
+ description: "Delete a project milestone.",
152
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project milestone to delete" }), }),
153
+ async execute(input, ctx) {
154
+ requireUnscoped(ctx, "projects.*");
155
+ const data = await gql(key(ctx), `mutation($id: String!) { projectMilestoneDelete(id: $id) { success } }`, { id: input.id });
156
+ return data.projectMilestoneDelete;
157
+ },
158
+ });
159
+ // Project updates
160
+ listAction("projectUpdate.list", "List project updates.", "projectUpdates", "ProjectUpdateFilter", PROJECT_UPDATE_FIELDS);
161
+ rl.registerAction("projectUpdate.create", {
162
+ description: "Post a status update on a project.",
163
+ inputSchema: t.Object({
164
+ projectId: t.String({ description: "The project to associate the project update with" }),
165
+ body: t.Optional(t.String({ description: "The content of the project update in markdown format" })),
166
+ health: t.Optional(t.String({ description: "The health of the project at the time of the update (ProjectUpdateHealthType: onTrack | atRisk | offTrack)" })),
167
+ isDiffHidden: t.Optional(t.Boolean({ description: "Whether the diff between the current update and the previous one should be hidden" })),
168
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
169
+ }),
170
+ async execute(input, ctx) {
171
+ requireUnscoped(ctx, "projects.*");
172
+ const data = await gql(key(ctx), `mutation($input: ProjectUpdateCreateInput!) { projectUpdateCreate(input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { input: input });
173
+ return data.projectUpdateCreate?.projectUpdate;
174
+ },
175
+ });
176
+ rl.registerAction("projectUpdate.update", {
177
+ description: "Update a project status update.",
178
+ inputSchema: t.Object({
179
+ id: t.String({ description: "The identifier of the project update to update" }),
180
+ body: t.Optional(t.String({ description: "The content of the project update in markdown format" })),
181
+ health: t.Optional(t.String({ description: "The health of the project at the time of the update (ProjectUpdateHealthType: onTrack | atRisk | offTrack)" })),
182
+ isDiffHidden: t.Optional(t.Boolean({ description: "Whether the diff between the current update and the previous one should be hidden" })),
183
+ }),
184
+ async execute(input, ctx) {
185
+ requireUnscoped(ctx, "projects.*");
186
+ const { id, ...fields } = input;
187
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateUpdateInput!) { projectUpdateUpdate(id: $id, input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { id, input: fields });
188
+ return data.projectUpdateUpdate?.projectUpdate;
189
+ },
190
+ });
191
+ rl.registerAction("projectUpdate.archive", {
192
+ description: "Archive a project status update.",
193
+ inputSchema: t.Object({ id: t.String({ description: "The identifier of the project update to archive" }), }),
194
+ async execute(input, ctx) {
195
+ requireUnscoped(ctx, "projects.*");
196
+ const data = await gql(key(ctx), `mutation($id: String!) { projectUpdateArchive(id: $id) { success } }`, { id: input.id });
197
+ return data.projectUpdateArchive;
198
+ },
199
+ });
200
+ }
@@ -0,0 +1,234 @@
1
+ import * as t from "typebox";
2
+ const GQL_URL = "https://api.linear.app/graphql";
3
+ export async function gql(apiKey, query, variables) {
4
+ const body = { query };
5
+ if (variables)
6
+ body.variables = variables;
7
+ const res = await fetch(GQL_URL, {
8
+ method: "POST",
9
+ headers: { Authorization: apiKey, "Content-Type": "application/json" },
10
+ body: JSON.stringify(body),
11
+ });
12
+ if (!res.ok)
13
+ throw new Error(`Linear API error ${res.status}: ${await res.text()}`);
14
+ const data = (await res.json());
15
+ if (data.errors)
16
+ throw new Error(`Linear GraphQL error: ${JSON.stringify(data.errors)}`);
17
+ return data.data;
18
+ }
19
+ export function key(ctx) {
20
+ return ctx.connection.config.apiKey;
21
+ }
22
+ export function scopeLabelIds(ctx) {
23
+ const raw = ctx.connection.config.scopeLabelIds;
24
+ if (Array.isArray(raw))
25
+ return raw.map(String).map((s) => s.trim()).filter(Boolean);
26
+ if (typeof raw !== "string")
27
+ return [];
28
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
29
+ }
30
+ export function isScoped(ctx) {
31
+ return scopeLabelIds(ctx).length > 0;
32
+ }
33
+ export function mergeIssueScopeFilter(ctx, filter) {
34
+ const ids = scopeLabelIds(ctx);
35
+ if (ids.length === 0)
36
+ return filter;
37
+ const scopeFilter = { labels: { id: { in: ids } } };
38
+ if (!filter || Object.keys(filter).length === 0)
39
+ return scopeFilter;
40
+ return { and: [filter, scopeFilter] };
41
+ }
42
+ export function issueHasScope(ctx, issue) {
43
+ const ids = new Set(scopeLabelIds(ctx));
44
+ if (ids.size === 0)
45
+ return true;
46
+ const labels = issue?.labels?.nodes;
47
+ return Array.isArray(labels) && labels.some((label) => ids.has(String(label.id)));
48
+ }
49
+ export async function getIssueForScope(ctx, issueId) {
50
+ const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { id identifier labels { nodes { id name } } } }`, { id: issueId });
51
+ return data.issue ?? null;
52
+ }
53
+ export async function assertIssueInScope(ctx, issueId) {
54
+ if (!isScoped(ctx))
55
+ return null;
56
+ const issue = await getIssueForScope(ctx, issueId);
57
+ if (!issue || !issueHasScope(ctx, issue))
58
+ throw new Error("Linear issue is not available to this scoped connection");
59
+ return issue;
60
+ }
61
+ export async function assertCommentInScope(ctx, commentId) {
62
+ if (!isScoped(ctx))
63
+ return;
64
+ const data = await gql(key(ctx), `query($id: String!) { comment(id: $id) { id issue { id identifier labels { nodes { id name } } } } }`, { id: commentId });
65
+ const issue = data.comment?.issue;
66
+ if (!issue || !issueHasScope(ctx, issue))
67
+ throw new Error("Linear comment is not available to this scoped connection");
68
+ }
69
+ export async function assertAttachmentInScope(ctx, attachmentId) {
70
+ if (!isScoped(ctx))
71
+ return;
72
+ const data = await gql(key(ctx), `query($id: String!) { attachment(id: $id) { id issue { id identifier labels { nodes { id name } } } } }`, { id: attachmentId });
73
+ const issue = data.attachment?.issue;
74
+ if (!issue || !issueHasScope(ctx, issue))
75
+ throw new Error("Linear attachment is not available to this scoped connection");
76
+ }
77
+ export function forbidScopeLabelRemoval(ctx, labelIds) {
78
+ const scoped = new Set(scopeLabelIds(ctx));
79
+ if (scoped.size === 0)
80
+ return;
81
+ const ids = Array.isArray(labelIds) ? labelIds.map(String) : [String(labelIds)];
82
+ if (ids.some((id) => scoped.has(id))) {
83
+ throw new Error("Cannot remove a required Linear scope label");
84
+ }
85
+ }
86
+ export function ensureScopeLabelsOnCreateOrReplace(ctx, labelIds) {
87
+ const scoped = scopeLabelIds(ctx);
88
+ if (scoped.length === 0)
89
+ return labelIds;
90
+ const ids = new Set(Array.isArray(labelIds) ? labelIds.map(String) : []);
91
+ for (const id of scoped)
92
+ ids.add(id);
93
+ return [...ids];
94
+ }
95
+ export function requireUnscoped(ctx, action) {
96
+ if (isScoped(ctx)) {
97
+ throw new Error(`${action} is not available to scoped Linear connections`);
98
+ }
99
+ }
100
+ export const ISSUE_FIELDS = `id identifier title description url priority estimate dueDate
101
+ state { id name type } assignee { id name email } creator { id name }
102
+ team { id key name } project { id name } cycle { id number name }
103
+ projectMilestone { id name } parent { id identifier }
104
+ labels { nodes { id name color } }
105
+ createdAt updatedAt completedAt canceledAt archivedAt`;
106
+ export const ISSUE_LITE = `id identifier title url priority state { id name type } assignee { id name } team { key } updatedAt`;
107
+ export const COMMENT_FIELDS = `id body url issue { id identifier } user { id name } parent { id } createdAt updatedAt editedAt resolvedAt`;
108
+ export const STATE_FIELDS = `id name type color position description team { id key }`;
109
+ export const LABEL_FIELDS = `id name color description isGroup parent { id name } team { id key } createdAt`;
110
+ export const PROJECT_FIELDS = `id name description url icon color priority progress health
111
+ state status { id name type } lead { id name } startDate targetDate
112
+ teams { nodes { id key } } createdAt updatedAt completedAt canceledAt`;
113
+ export const MILESTONE_FIELDS = `id name description targetDate sortOrder project { id name } createdAt updatedAt`;
114
+ export const PROJECT_UPDATE_FIELDS = `id body health url user { id name } project { id name } createdAt`;
115
+ export const FEED_ITEM_FIELDS = `id createdAt updatedAt archivedAt team { id key name } user { id name }
116
+ projectUpdate { ${PROJECT_UPDATE_FIELDS} }
117
+ initiativeUpdate { id body health url user { id name } initiative { id name } createdAt }
118
+ post { id title body slugId type creator { id name } createdAt updatedAt }`;
119
+ export const CUSTOM_VIEW_FIELDS = `id name description icon color shared slugId modelName
120
+ filterData projectFilterData initiativeFilterData feedItemFilterData
121
+ team { id key name } owner { id name } creator { id name }
122
+ createdAt updatedAt archivedAt`;
123
+ export const CYCLE_FIELDS = `id number name description startsAt endsAt completedAt progress team { id key } createdAt`;
124
+ export const INITIATIVE_FIELDS = `id name description url icon color status targetDate owner { id name }
125
+ projects { nodes { id name } } createdAt updatedAt completedAt`;
126
+ export const TEAM_FIELDS = `id key name description icon color private timezone
127
+ cyclesEnabled cycleDuration issueEstimationType triageEnabled
128
+ parent { id key } createdAt`;
129
+ export const USER_FIELDS = `id name displayName email avatarUrl active admin guest
130
+ isMe statusEmoji statusLabel createdAt`;
131
+ export const ATTACHMENT_FIELDS = `id title subtitle url sourceType groupBySource metadata
132
+ issue { id identifier } creator { id name } createdAt updatedAt`;
133
+ export const ORG_FIELDS = `id name urlKey logoUrl userCount createdIssueCount
134
+ periodUploadVolume samlEnabled scimEnabled createdAt`;
135
+ export const WEBHOOK_FIELDS = `id label url enabled resourceTypes secret
136
+ team { id key } allPublicTeams createdAt`;
137
+ export function buildConnArgs(opts, filterTypeName) {
138
+ const declParts = [];
139
+ const callParts = [`first: $first`];
140
+ const vars = { first: opts.limit ?? 50 };
141
+ declParts.push(`$first: Int`);
142
+ if (filterTypeName && opts.filter !== undefined) {
143
+ declParts.push(`$filter: ${filterTypeName}`);
144
+ callParts.push(`filter: $filter`);
145
+ vars.filter = opts.filter;
146
+ }
147
+ if (opts.includeArchived !== undefined) {
148
+ declParts.push(`$includeArchived: Boolean`);
149
+ callParts.push(`includeArchived: $includeArchived`);
150
+ vars.includeArchived = opts.includeArchived;
151
+ }
152
+ if (opts.orderBy !== undefined) {
153
+ declParts.push(`$orderBy: PaginationOrderBy`);
154
+ callParts.push(`orderBy: $orderBy`);
155
+ vars.orderBy = opts.orderBy;
156
+ }
157
+ if (opts.after !== undefined) {
158
+ declParts.push(`$after: String`);
159
+ callParts.push(`after: $after`);
160
+ vars.after = opts.after;
161
+ }
162
+ if (opts.before !== undefined) {
163
+ declParts.push(`$before: String`);
164
+ callParts.push(`before: $before`);
165
+ vars.before = opts.before;
166
+ }
167
+ return {
168
+ argsDecl: `(${declParts.join(", ")})`,
169
+ argsCall: `(${callParts.join(", ")})`,
170
+ vars,
171
+ };
172
+ }
173
+ export const LIST_INPUT_SCHEMA = {
174
+ limit: t.Optional(t.Number({ description: "Max results (default 50, max 250)" })),
175
+ filter: t.Optional(t.Object({}, { description: "Linear filter object (see schema for the resource)" })),
176
+ includeArchived: t.Optional(t.Boolean({ description: "Include archived items" })),
177
+ orderBy: t.Optional(t.String({ description: "createdAt | updatedAt" })),
178
+ after: t.Optional(t.String({ description: "Cursor for forward pagination" })),
179
+ before: t.Optional(t.String({ description: "Cursor for backward pagination" })),
180
+ };
181
+ export function bindListAction(rl) {
182
+ return (...args) => registerListAction(rl, ...args);
183
+ }
184
+ export function bindGetAction(rl) {
185
+ return (...args) => registerGetAction(rl, ...args);
186
+ }
187
+ const SCOPED_BLOCKED_ROOT_FIELDS = new Set([
188
+ "attachments",
189
+ "comments",
190
+ "customView",
191
+ "customViews",
192
+ "cycle",
193
+ "cycles",
194
+ "initiative",
195
+ "initiatives",
196
+ "project",
197
+ "projects",
198
+ "projectMilestone",
199
+ "projectMilestones",
200
+ "projectUpdates",
201
+ "user",
202
+ "users",
203
+ "webhook",
204
+ "webhooks",
205
+ ]);
206
+ function requireRootFieldAvailable(ctx, action, rootField) {
207
+ if (SCOPED_BLOCKED_ROOT_FIELDS.has(rootField))
208
+ requireUnscoped(ctx, action);
209
+ }
210
+ export function registerListAction(rl, name, description, rootField, filterTypeName, selection) {
211
+ rl.registerAction(name, {
212
+ description,
213
+ inputSchema: t.Object(LIST_INPUT_SCHEMA),
214
+ async execute(input, ctx) {
215
+ requireRootFieldAvailable(ctx, name, rootField);
216
+ const opts = (input ?? {});
217
+ const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
218
+ const data = await gql(key(ctx), `query${argsDecl} { ${rootField}${argsCall} { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }`, vars);
219
+ const conn = data[rootField];
220
+ return { nodes: conn.nodes, pageInfo: conn.pageInfo };
221
+ },
222
+ });
223
+ }
224
+ export function registerGetAction(rl, name, description, rootField, selection) {
225
+ rl.registerAction(name, {
226
+ description,
227
+ inputSchema: t.Object({ id: t.String({ description: "Identifier or slug" }) }),
228
+ async execute(input, ctx) {
229
+ requireRootFieldAvailable(ctx, name, rootField);
230
+ const data = await gql(key(ctx), `query($id: String!) { ${rootField}(id: $id) { ${selection} } }`, { id: input.id });
231
+ return data[rootField];
232
+ },
233
+ });
234
+ }
@@ -0,0 +1,41 @@
1
+ import * as t from "typebox";
2
+ import { STATE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
+ export function registerStateActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("state.list", "List workflow states. Filter by team for team-scoped states.", "workflowStates", "WorkflowStateFilter", STATE_FIELDS);
7
+ getAction("state.get", "Get a workflow state by ID.", "workflowState", STATE_FIELDS);
8
+ rl.registerAction("state.create", {
9
+ description: "Create a workflow state in a team.",
10
+ inputSchema: t.Object({
11
+ teamId: t.String({ description: "The team associated with the state" }),
12
+ name: t.String({ description: "The name of the state" }),
13
+ type: t.String({ description: "The workflow state type which categorizes the state. Valid values: backlog, unstarted, started, completed, canceled" }),
14
+ color: t.String({ description: "The color of the state (hex, e.g. #6B7280)" }),
15
+ description: t.Optional(t.String({ description: "The description of the state" })),
16
+ position: t.Optional(t.Number({ description: "The position of the state (Float)" })),
17
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
18
+ }),
19
+ async execute(input, ctx) {
20
+ requireUnscoped(ctx, "state.create");
21
+ const data = await gql(key(ctx), `mutation($input: WorkflowStateCreateInput!) { workflowStateCreate(input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { input: input });
22
+ return data.workflowStateCreate?.workflowState;
23
+ },
24
+ });
25
+ rl.registerAction("state.update", {
26
+ description: "Update a workflow state. Type cannot be changed after creation.",
27
+ inputSchema: t.Object({
28
+ id: t.String({ description: "The identifier of the state to update" }),
29
+ name: t.Optional(t.String({ description: "The name of the state" })),
30
+ color: t.Optional(t.String({ description: "The color of the state (hex)" })),
31
+ description: t.Optional(t.String({ description: "The description of the state" })),
32
+ position: t.Optional(t.Number({ description: "The position of the state (Float)" })),
33
+ }),
34
+ async execute(input, ctx) {
35
+ requireUnscoped(ctx, "state.update");
36
+ const { id, ...fields } = input;
37
+ const data = await gql(key(ctx), `mutation($id: String!, $input: WorkflowStateUpdateInput!) { workflowStateUpdate(id: $id, input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { id, input: fields });
38
+ return data.workflowStateUpdate?.workflowState;
39
+ },
40
+ });
41
+ }
@@ -0,0 +1,77 @@
1
+ import * as t from "typebox";
2
+ import { TEAM_FIELDS, USER_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped, } from "./shared.js";
3
+ export function registerTeamActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("team.list", "List teams whose issues you can access.", "teams", "TeamFilter", TEAM_FIELDS);
7
+ getAction("team.get", "Get a team by ID or key.", "team", TEAM_FIELDS);
8
+ rl.registerAction("team.create", {
9
+ description: "Create a team. Most settings have sensible defaults.",
10
+ inputSchema: t.Object({
11
+ name: t.String({ description: "The name of the team" }),
12
+ key: t.Optional(t.String({ description: "The key of the team. If not given, the key will be generated based on the name" })),
13
+ description: t.Optional(t.String({ description: "The description of the team" })),
14
+ icon: t.Optional(t.String({ description: "The icon of the team" })),
15
+ color: t.Optional(t.String({ description: "The color of the team (hex)" })),
16
+ private: t.Optional(t.Boolean({ description: "Whether the team is private" })),
17
+ timezone: t.Optional(t.String({ description: "The timezone of the team" })),
18
+ cyclesEnabled: t.Optional(t.Boolean({ description: "Whether the team uses cycles" })),
19
+ cycleDuration: t.Optional(t.Number({ description: "The duration of each cycle in weeks (Int)" })),
20
+ cycleCooldownTime: t.Optional(t.Number({ description: "The cooldown time after each cycle in weeks (Int)" })),
21
+ cycleStartDay: t.Optional(t.Number({ description: "The day of the week that a new cycle starts. 0=Sun..6=Sat (Float)" })),
22
+ upcomingCycleCount: t.Optional(t.Number({ description: "How many upcoming cycles to create (Float)" })),
23
+ issueEstimationType: t.Optional(t.String({ description: "The issue estimation type: notUsed | exponential | fibonacci | linear | tShirt" })),
24
+ triageEnabled: t.Optional(t.Boolean({ description: "Whether triage mode is enabled for the team" })),
25
+ parentId: t.Optional(t.String({ description: "The parent team ID (for sub-teams)" })),
26
+ id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
27
+ copySettingsFromTeamId: t.Optional(t.String({ description: "The team id to copy settings from, if any" })),
28
+ }),
29
+ async execute(input, ctx) {
30
+ requireUnscoped(ctx, "team.create");
31
+ const { copySettingsFromTeamId, ...fields } = input;
32
+ const data = await gql(key(ctx), `mutation($input: TeamCreateInput!, $copySettingsFromTeamId: String) { teamCreate(input: $input, copySettingsFromTeamId: $copySettingsFromTeamId) { success team { ${TEAM_FIELDS} } } }`, { input: fields, copySettingsFromTeamId: copySettingsFromTeamId ?? null });
33
+ return data.teamCreate?.team;
34
+ },
35
+ });
36
+ rl.registerAction("team.update", {
37
+ description: "Update a team. Requires team owner or workspace admin permissions.",
38
+ inputSchema: t.Object({
39
+ id: t.String({ description: "The identifier of the team to update" }),
40
+ name: t.Optional(t.String({ description: "The name of the team" })),
41
+ key: t.Optional(t.String({ description: "The key of the team" })),
42
+ description: t.Optional(t.String({ description: "The description of the team" })),
43
+ icon: t.Optional(t.String({ description: "The icon of the team" })),
44
+ color: t.Optional(t.String({ description: "The color of the team (hex)" })),
45
+ private: t.Optional(t.Boolean({ description: "Whether the team is private" })),
46
+ timezone: t.Optional(t.String({ description: "The timezone of the team" })),
47
+ cyclesEnabled: t.Optional(t.Boolean({ description: "Whether the team uses cycles" })),
48
+ cycleDuration: t.Optional(t.Number({ description: "The duration of each cycle in weeks (Int)" })),
49
+ cycleCooldownTime: t.Optional(t.Number({ description: "The cooldown time after each cycle in weeks (Int)" })),
50
+ cycleStartDay: t.Optional(t.Number({ description: "The day of the week that a new cycle starts. 0=Sun..6=Sat (Float)" })),
51
+ upcomingCycleCount: t.Optional(t.Number({ description: "How many upcoming cycles to create (Float)" })),
52
+ issueEstimationType: t.Optional(t.String({ description: "The issue estimation type: notUsed | exponential | fibonacci | linear | tShirt" })),
53
+ triageEnabled: t.Optional(t.Boolean({ description: "Whether triage mode is enabled for the team" })),
54
+ }),
55
+ async execute(input, ctx) {
56
+ requireUnscoped(ctx, "team.update");
57
+ const { id, ...fields } = input;
58
+ const data = await gql(key(ctx), `mutation($id: String!, $input: TeamUpdateInput!) { teamUpdate(id: $id, input: $input) { success team { ${TEAM_FIELDS} } } }`, { id, input: fields });
59
+ return data.teamUpdate?.team;
60
+ },
61
+ });
62
+ rl.registerAction("team.members", {
63
+ description: "List members of a team.",
64
+ inputSchema: t.Object({
65
+ teamId: t.String({ description: "The identifier of the team" }),
66
+ limit: t.Optional(t.Number({ description: "Max members to return (default 50)" })),
67
+ }),
68
+ async execute(input, ctx) {
69
+ requireUnscoped(ctx, "team.members");
70
+ const { teamId, limit } = input;
71
+ const data = await gql(key(ctx), `query($id: String!, $first: Int) {
72
+ team(id: $id) { members(first: $first) { nodes { ${USER_FIELDS} } } }
73
+ }`, { id: teamId, first: limit ?? 50 });
74
+ return data.team?.members?.nodes;
75
+ },
76
+ });
77
+ }
@@ -0,0 +1,37 @@
1
+ import * as t from "typebox";
2
+ import { USER_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
+ export function registerUserActions(rl) {
4
+ const listAction = bindListAction(rl);
5
+ const getAction = bindGetAction(rl);
6
+ listAction("user.list", "List users in the workspace.", "users", "UserFilter", USER_FIELDS);
7
+ getAction("user.get", "Get a user by ID. Use 'me' to reference the authenticated user.", "user", USER_FIELDS);
8
+ rl.registerAction("user.me", {
9
+ description: "Get the authenticated user.",
10
+ inputSchema: t.Object({}),
11
+ async execute(_input, ctx) {
12
+ const data = await gql(key(ctx), `query { viewer { ${USER_FIELDS} } }`);
13
+ return data.viewer;
14
+ },
15
+ });
16
+ rl.registerAction("user.update", {
17
+ description: "Update a user. Use id='me' to update the authenticated user.",
18
+ inputSchema: t.Object({
19
+ id: t.String({ description: "The identifier of the user to update. Use 'me' to reference the currently authenticated user" }),
20
+ name: t.Optional(t.String({ description: "The name of the user" })),
21
+ displayName: t.Optional(t.String({ description: "The display name of the user" })),
22
+ description: t.Optional(t.String({ description: "The user description or short bio" })),
23
+ avatarUrl: t.Optional(t.String({ description: "The avatar image URL of the user" })),
24
+ timezone: t.Optional(t.String({ description: "The local timezone of the user" })),
25
+ title: t.Optional(t.String({ description: "The user's job title" })),
26
+ statusEmoji: t.Optional(t.String({ description: "The emoji part of the user status" })),
27
+ statusLabel: t.Optional(t.String({ description: "The label part of the user status" })),
28
+ statusUntilAt: t.Optional(t.String({ description: "When the user status should be cleared (DateTime)" })),
29
+ }),
30
+ async execute(input, ctx) {
31
+ requireUnscoped(ctx, "user.update");
32
+ const { id, ...fields } = input;
33
+ const data = await gql(key(ctx), `mutation($id: String!, $input: UserUpdateInput!) { userUpdate(id: $id, input: $input) { success user { ${USER_FIELDS} } } }`, { id, input: fields });
34
+ return data.userUpdate?.user;
35
+ },
36
+ });
37
+ }