runline 0.5.3 → 0.6.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.
@@ -15,186 +15,1044 @@ async function gql(apiKey, query, variables) {
15
15
  throw new Error(`Linear GraphQL error: ${JSON.stringify(data.errors)}`);
16
16
  return data.data;
17
17
  }
18
+ // ---------- selection sets (kept compact, repeated by name) ----------
19
+ const ISSUE_FIELDS = `id identifier title description url priority estimate dueDate
20
+ state { id name type } assignee { id name email } creator { id name }
21
+ team { id key name } project { id name } cycle { id number name }
22
+ projectMilestone { id name } parent { id identifier }
23
+ labels { nodes { id name color } }
24
+ createdAt updatedAt completedAt canceledAt archivedAt`;
25
+ const ISSUE_LITE = `id identifier title url priority state { id name type } assignee { id name } team { key } updatedAt`;
26
+ const COMMENT_FIELDS = `id body url issue { id identifier } user { id name } parent { id } createdAt updatedAt editedAt resolvedAt`;
27
+ const STATE_FIELDS = `id name type color position description team { id key }`;
28
+ const LABEL_FIELDS = `id name color description isGroup parent { id name } team { id key } createdAt`;
29
+ const PROJECT_FIELDS = `id name description url icon color priority progress health
30
+ state status { id name type } lead { id name } startDate targetDate
31
+ teams { nodes { id key } } createdAt updatedAt completedAt canceledAt`;
32
+ const MILESTONE_FIELDS = `id name description targetDate sortOrder project { id name } createdAt updatedAt`;
33
+ const PROJECT_UPDATE_FIELDS = `id body health url user { id name } project { id name } createdAt`;
34
+ const CYCLE_FIELDS = `id number name description startsAt endsAt completedAt progress team { id key } createdAt`;
35
+ const INITIATIVE_FIELDS = `id name description url icon color status targetDate owner { id name }
36
+ projects { nodes { id name } } createdAt updatedAt completedAt`;
37
+ const TEAM_FIELDS = `id key name description icon color private timezone
38
+ cyclesEnabled cycleDuration issueEstimationType triageEnabled
39
+ parent { id key } createdAt`;
40
+ const USER_FIELDS = `id name displayName email avatarUrl active admin guest
41
+ isMe statusEmoji statusLabel createdAt`;
42
+ const ATTACHMENT_FIELDS = `id title subtitle url sourceType groupBySource metadata
43
+ issue { id identifier } creator { id name } createdAt updatedAt`;
44
+ const ORG_FIELDS = `id name urlKey logoUrl userCount createdIssueCount
45
+ periodUploadVolume samlEnabled scimEnabled createdAt`;
46
+ const WEBHOOK_FIELDS = `id label url enabled resourceTypes secret
47
+ team { id key } allPublicTeams createdAt`;
48
+ function buildConnArgs(opts, filterTypeName) {
49
+ const declParts = [];
50
+ const callParts = [`first: $first`];
51
+ const vars = { first: opts.limit ?? 50 };
52
+ declParts.push(`$first: Int`);
53
+ if (filterTypeName && opts.filter !== undefined) {
54
+ declParts.push(`$filter: ${filterTypeName}`);
55
+ callParts.push(`filter: $filter`);
56
+ vars.filter = opts.filter;
57
+ }
58
+ if (opts.includeArchived !== undefined) {
59
+ declParts.push(`$includeArchived: Boolean`);
60
+ callParts.push(`includeArchived: $includeArchived`);
61
+ vars.includeArchived = opts.includeArchived;
62
+ }
63
+ if (opts.orderBy !== undefined) {
64
+ declParts.push(`$orderBy: PaginationOrderBy`);
65
+ callParts.push(`orderBy: $orderBy`);
66
+ vars.orderBy = opts.orderBy;
67
+ }
68
+ if (opts.after !== undefined) {
69
+ declParts.push(`$after: String`);
70
+ callParts.push(`after: $after`);
71
+ vars.after = opts.after;
72
+ }
73
+ if (opts.before !== undefined) {
74
+ declParts.push(`$before: String`);
75
+ callParts.push(`before: $before`);
76
+ vars.before = opts.before;
77
+ }
78
+ return {
79
+ argsDecl: `(${declParts.join(", ")})`,
80
+ argsCall: `(${callParts.join(", ")})`,
81
+ vars,
82
+ };
83
+ }
84
+ // Common list-input schema reused across resources
85
+ const LIST_INPUT_SCHEMA = {
86
+ limit: { type: "number", required: false, description: "Max results (default 50, max 250)" },
87
+ filter: { type: "object", required: false, description: "Linear filter object (see schema for the resource)" },
88
+ includeArchived: { type: "boolean", required: false, description: "Include archived items" },
89
+ orderBy: { type: "string", required: false, description: "createdAt | updatedAt" },
90
+ after: { type: "string", required: false, description: "Cursor for forward pagination" },
91
+ before: { type: "string", required: false, description: "Cursor for backward pagination" },
92
+ };
93
+ // ---------- plugin ----------
18
94
  export default function linear(rl) {
19
95
  rl.setName("linear");
20
- rl.setVersion("0.1.0");
96
+ rl.setVersion("0.3.0");
21
97
  rl.setConnectionSchema({
22
98
  apiKey: {
23
99
  type: "string",
24
100
  required: true,
25
- description: "Linear API key",
101
+ description: "Linear API key (https://linear.app/settings/account/security)",
26
102
  env: "LINEAR_API_KEY",
27
103
  },
28
104
  });
29
105
  const key = (ctx) => ctx.connection.config.apiKey;
30
- rl.registerAction("issue.create", {
31
- description: "Create an issue",
32
- inputSchema: {
33
- teamId: { type: "string", required: true, description: "Team ID" },
34
- title: { type: "string", required: true, description: "Issue title" },
35
- description: {
36
- type: "string",
37
- required: false,
38
- description: "Issue description (markdown)",
39
- },
40
- assigneeId: {
41
- type: "string",
42
- required: false,
43
- description: "Assignee user ID",
44
- },
45
- priority: {
46
- type: "number",
47
- required: false,
48
- description: "Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)",
106
+ // Shared helpers for connection-style listing
107
+ function listAction(name, description, rootField, filterTypeName, selection) {
108
+ rl.registerAction(name, {
109
+ description,
110
+ inputSchema: { ...LIST_INPUT_SCHEMA },
111
+ async execute(input, ctx) {
112
+ const opts = (input ?? {});
113
+ const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
114
+ const data = await gql(key(ctx), `query${argsDecl} { ${rootField}${argsCall} { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }`, vars);
115
+ const conn = data[rootField];
116
+ return { nodes: conn.nodes, pageInfo: conn.pageInfo };
49
117
  },
50
- stateId: {
51
- type: "string",
52
- required: false,
53
- description: "Workflow state ID",
54
- },
55
- labelIds: { type: "array", required: false, description: "Label IDs" },
56
- parentId: {
57
- type: "string",
58
- required: false,
59
- description: "Parent issue ID (for sub-issues)",
118
+ });
119
+ }
120
+ function getAction(name, description, rootField, selection) {
121
+ rl.registerAction(name, {
122
+ description,
123
+ inputSchema: { id: { type: "string", required: true, description: "Identifier or slug" } },
124
+ async execute(input, ctx) {
125
+ const data = await gql(key(ctx), `query($id: String!) { ${rootField}(id: $id) { ${selection} } }`, { id: input.id });
126
+ return data[rootField];
60
127
  },
128
+ });
129
+ }
130
+ // =========================================================
131
+ // Issues
132
+ // =========================================================
133
+ rl.registerAction("issue.create", {
134
+ description: "Create an issue. teamId is required; title is required unless a template is applied.",
135
+ inputSchema: {
136
+ teamId: { type: "string", required: true, description: "The identifier of the team associated with the issue" },
137
+ title: { type: "string", required: true, description: "The title of the issue" },
138
+ description: { type: "string", required: false, description: "The issue description in markdown format" },
139
+ assigneeId: { type: "string", required: false, description: "The identifier of the user to assign the issue to" },
140
+ priority: { type: "number", required: false, description: "Priority. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" },
141
+ stateId: { type: "string", required: false, description: "The team workflow state of the issue" },
142
+ labelIds: { type: "array", required: false, description: "The identifiers of the issue labels associated with this ticket" },
143
+ parentId: { type: "string", required: false, description: "The identifier of the parent issue. UUID or issue identifier (e.g., 'LIN-123')" },
144
+ projectId: { type: "string", required: false, description: "The project associated with the issue" },
145
+ projectMilestoneId: { type: "string", required: false, description: "The project milestone associated with the issue" },
146
+ cycleId: { type: "string", required: false, description: "The cycle associated with the issue" },
147
+ estimate: { type: "number", required: false, description: "The estimated complexity of the issue (Int)" },
148
+ dueDate: { type: "string", required: false, description: "The date at which the issue is due (TimelessDate, YYYY-MM-DD)" },
149
+ subscriberIds: { type: "array", required: false, description: "The identifiers of the users subscribing to this ticket" },
150
+ templateId: { type: "string", required: false, description: "The identifier of a template the issue should be created from" },
151
+ useDefaultTemplate: { type: "boolean", required: false, description: "Apply the team's default template based on the user's membership" },
152
+ sortOrder: { type: "number", required: false, description: "The position of the issue related to other issues (Float)" },
153
+ subIssueSortOrder: { type: "number", required: false, description: "The position of the issue in its parent's sub-issue list (Float)" },
154
+ releaseIds: { type: "array", required: false, description: "The identifiers of the releases to associate with this issue" },
155
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
61
156
  },
62
157
  async execute(input, ctx) {
63
- const { teamId, title, description: desc, assigneeId, priority, stateId, labelIds, parentId, } = input;
64
- const vars = { teamId, title };
65
- if (desc)
66
- vars.description = desc;
67
- if (assigneeId)
68
- vars.assigneeId = assigneeId;
69
- if (priority !== undefined)
70
- vars.priority = priority;
71
- if (stateId)
72
- vars.stateId = stateId;
73
- if (labelIds)
74
- vars.labelIds = labelIds;
75
- if (parentId)
76
- vars.parentId = parentId;
77
- const data = await gql(key(ctx), `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { id identifier title url } } }`, { input: vars });
158
+ const fields = input;
159
+ const data = await gql(key(ctx), `mutation($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { input: fields });
78
160
  return data.issueCreate?.issue;
79
161
  },
80
162
  });
81
163
  rl.registerAction("issue.get", {
82
- description: "Get an issue by ID",
83
- inputSchema: {
84
- issueId: { type: "string", required: true, description: "Issue ID" },
85
- },
164
+ description: "Get an issue by ID or identifier (e.g. 'THE-154')",
165
+ inputSchema: { issueId: { type: "string", required: true } },
86
166
  async execute(input, ctx) {
87
- const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { id identifier title description url priority state { id name } assignee { id name } labels { nodes { id name } } createdAt updatedAt } }`, { id: input.issueId });
167
+ const data = await gql(key(ctx), `query($id: String!) { issue(id: $id) { ${ISSUE_FIELDS} } }`, { id: input.issueId });
88
168
  return data.issue;
89
169
  },
90
170
  });
91
171
  rl.registerAction("issue.list", {
92
- description: "List issues",
172
+ description: "List issues. Pass `filter` for state/label/project/etc. Default hides archived.",
93
173
  inputSchema: {
94
- limit: {
95
- type: "number",
96
- required: false,
97
- description: "Max results (default: 50)",
98
- },
99
- teamId: {
100
- type: "string",
101
- required: false,
102
- description: "Filter by team",
103
- },
104
- assigneeId: {
105
- type: "string",
106
- required: false,
107
- description: "Filter by assignee",
108
- },
174
+ ...LIST_INPUT_SCHEMA,
175
+ teamId: { type: "string", required: false, description: "Convenience: filter by team" },
176
+ assigneeId: { type: "string", required: false, description: "Convenience: filter by assignee" },
109
177
  },
110
178
  async execute(input, ctx) {
111
- const { limit = 50, teamId, assigneeId, } = (input ?? {});
112
- let filter = "";
113
- const filterParts = [];
114
- if (teamId)
115
- filterParts.push(`team: { id: { eq: "${teamId}" } }`);
116
- if (assigneeId)
117
- filterParts.push(`assignee: { id: { eq: "${assigneeId}" } }`);
118
- if (filterParts.length > 0)
119
- filter = `, filter: { ${filterParts.join(", ")} }`;
120
- const data = await gql(key(ctx), `query { issues(first: ${limit}${filter}) { nodes { id identifier title url priority state { name } assignee { name } createdAt } } }`);
121
- return data.issues?.nodes;
179
+ const opts = (input ?? {});
180
+ // Merge convenience filters into `filter`
181
+ const merged = { ...(opts.filter ?? {}) };
182
+ if (opts.teamId)
183
+ merged.team = { id: { eq: opts.teamId } };
184
+ if (opts.assigneeId)
185
+ merged.assignee = { id: { eq: opts.assigneeId } };
186
+ const filter = Object.keys(merged).length > 0 ? merged : undefined;
187
+ const { argsDecl, argsCall, vars } = buildConnArgs({ ...opts, filter }, "IssueFilter");
188
+ const data = await gql(key(ctx), `query${argsDecl} { issues${argsCall} { nodes { ${ISSUE_LITE} } pageInfo { hasNextPage endCursor } } }`, vars);
189
+ const conn = data.issues;
190
+ return { nodes: conn.nodes, pageInfo: conn.pageInfo };
122
191
  },
123
192
  });
124
193
  rl.registerAction("issue.update", {
125
- description: "Update an issue",
126
- inputSchema: {
127
- issueId: { type: "string", required: true, description: "Issue ID" },
128
- title: { type: "string", required: false, description: "New title" },
129
- description: {
130
- type: "string",
131
- required: false,
132
- description: "New description",
133
- },
134
- assigneeId: {
135
- type: "string",
136
- required: false,
137
- description: "Assignee ID",
138
- },
139
- stateId: { type: "string", required: false, description: "State ID" },
140
- priority: { type: "number", required: false, description: "Priority" },
141
- labelIds: { type: "array", required: false, description: "Label IDs" },
194
+ description: "Update an issue. All fields optional; only provided fields are updated.",
195
+ inputSchema: {
196
+ issueId: { type: "string", required: true, description: "The identifier of the issue to update" },
197
+ title: { type: "string", required: false, description: "The issue title" },
198
+ description: { type: "string", required: false, description: "The issue description in markdown format" },
199
+ assigneeId: { type: "string", required: false, description: "The identifier of the user to assign the issue to" },
200
+ stateId: { type: "string", required: false, description: "The team workflow state of the issue" },
201
+ priority: { type: "number", required: false, description: "Priority. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" },
202
+ labelIds: { type: "array", required: false, description: "The identifiers of the issue labels associated with this ticket (replaces all)" },
203
+ addedLabelIds: { type: "array", required: false, description: "The identifiers of issue labels to be added to this issue" },
204
+ removedLabelIds: { type: "array", required: false, description: "The identifiers of issue labels to be removed from this issue" },
205
+ projectId: { type: "string", required: false, description: "The project associated with the issue" },
206
+ projectMilestoneId: { type: "string", required: false, description: "The project milestone associated with the issue" },
207
+ cycleId: { type: "string", required: false, description: "The cycle associated with the issue" },
208
+ parentId: { type: "string", required: false, description: "The identifier of the parent issue. UUID or issue identifier (e.g., 'LIN-123')" },
209
+ teamId: { type: "string", required: false, description: "The identifier of the team associated with the issue (move issue to a different team)" },
210
+ estimate: { type: "number", required: false, description: "The estimated complexity of the issue (Int)" },
211
+ dueDate: { type: "string", required: false, description: "The date at which the issue is due (TimelessDate, YYYY-MM-DD)" },
212
+ subscriberIds: { type: "array", required: false, description: "The identifiers of the users subscribing to this ticket" },
213
+ sortOrder: { type: "number", required: false, description: "The position of the issue related to other issues (Float)" },
214
+ subIssueSortOrder: { type: "number", required: false, description: "The position of the issue in its parent's sub-issue list (Float)" },
215
+ snoozedUntilAt: { type: "string", required: false, description: "The time until which the issue will be snoozed in Triage view (DateTime)" },
216
+ releaseIds: { type: "array", required: false, description: "The identifiers of the releases associated with this issue (replaces all)" },
217
+ addedReleaseIds: { type: "array", required: false, description: "The identifiers of releases to be added to this issue" },
218
+ removedReleaseIds: { type: "array", required: false, description: "The identifiers of releases to be removed from this issue" },
219
+ trashed: { type: "boolean", required: false, description: "Whether the issue has been trashed" },
142
220
  },
143
221
  async execute(input, ctx) {
144
222
  const { issueId, ...fields } = input;
145
- const data = await gql(key(ctx), `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { id identifier title url } } }`, { id: issueId, input: fields });
223
+ const data = await gql(key(ctx), `mutation($id: String!, $input: IssueUpdateInput!) { issueUpdate(id: $id, input: $input) { success issue { ${ISSUE_FIELDS} } } }`, { id: issueId, input: fields });
146
224
  return data.issueUpdate?.issue;
147
225
  },
148
226
  });
149
227
  rl.registerAction("issue.delete", {
150
- description: "Delete an issue",
228
+ description: "Trash (soft-delete) an issue. Pass permanentlyDelete=true to bypass 30d grace period (admin only).",
151
229
  inputSchema: {
152
- issueId: { type: "string", required: true, description: "Issue ID" },
230
+ issueId: { type: "string", required: true },
231
+ permanentlyDelete: { type: "boolean", required: false },
153
232
  },
154
233
  async execute(input, ctx) {
155
- const data = await gql(key(ctx), `mutation($id: String!) { issueDelete(id: $id) { success } }`, { id: input.issueId });
234
+ const { issueId, permanentlyDelete } = input;
235
+ const data = await gql(key(ctx), `mutation($id: String!, $perm: Boolean) { issueDelete(id: $id, permanentlyDelete: $perm) { success } }`, { id: issueId, perm: permanentlyDelete ?? null });
156
236
  return data.issueDelete;
157
237
  },
158
238
  });
159
- rl.registerAction("issue.addComment", {
160
- description: "Add a comment to an issue",
239
+ rl.registerAction("issue.archive", {
240
+ description: "Archive an issue.",
161
241
  inputSchema: {
162
- issueId: { type: "string", required: true, description: "Issue ID" },
163
- body: {
164
- type: "string",
165
- required: true,
166
- description: "Comment body (markdown)",
167
- },
242
+ issueId: { type: "string", required: true },
243
+ trash: { type: "boolean", required: false },
168
244
  },
169
245
  async execute(input, ctx) {
170
- const { issueId, body: commentBody } = input;
171
- const data = await gql(key(ctx), `mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { id body createdAt } } }`, { input: { issueId, body: commentBody } });
172
- return data.commentCreate?.comment;
246
+ const { issueId, trash } = input;
247
+ const data = await gql(key(ctx), `mutation($id: String!, $trash: Boolean) { issueArchive(id: $id, trash: $trash) { success } }`, { id: issueId, trash: trash ?? null });
248
+ return data.issueArchive;
249
+ },
250
+ });
251
+ rl.registerAction("issue.unarchive", {
252
+ description: "Unarchive an issue.",
253
+ inputSchema: { issueId: { type: "string", required: true } },
254
+ async execute(input, ctx) {
255
+ const data = await gql(key(ctx), `mutation($id: String!) { issueUnarchive(id: $id) { success } }`, { id: input.issueId });
256
+ return data.issueUnarchive;
257
+ },
258
+ });
259
+ rl.registerAction("issue.search", {
260
+ description: "Search issues by text query using full-text and vector search. Rate-limited to 30 req/min.",
261
+ inputSchema: {
262
+ term: { type: "string", required: true, description: "Search string to look for" },
263
+ limit: { type: "number", required: false, description: "Max results (forward pagination, default 50)" },
264
+ filter: { type: "object", required: false, description: "Optional IssueFilter" },
265
+ includeComments: { type: "boolean", required: false, description: "Should associated comments be searched (default false)" },
266
+ includeArchived: { type: "boolean", required: false, description: "Should archived resources be included (default false)" },
267
+ teamId: { type: "string", required: false, description: "UUID of a team to boost in search results" },
268
+ orderBy: { type: "string", required: false, description: "PaginationOrderBy: createdAt | updatedAt" },
269
+ after: { type: "string", required: false, description: "Cursor for forward pagination" },
270
+ before: { type: "string", required: false, description: "Cursor for backward pagination" },
271
+ },
272
+ async execute(input, ctx) {
273
+ const opts = input;
274
+ 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) {
275
+ searchIssues(term: $term, first: $first, filter: $filter, includeComments: $includeComments, includeArchived: $includeArchived, teamId: $teamId, orderBy: $orderBy, after: $after, before: $before) {
276
+ nodes { ${ISSUE_LITE} }
277
+ totalCount
278
+ pageInfo { hasNextPage endCursor }
279
+ }
280
+ }`, {
281
+ term: opts.term,
282
+ first: opts.limit ?? 50,
283
+ filter: opts.filter ?? null,
284
+ includeComments: opts.includeComments ?? null,
285
+ includeArchived: opts.includeArchived ?? null,
286
+ teamId: opts.teamId ?? null,
287
+ orderBy: opts.orderBy ?? null,
288
+ after: opts.after ?? null,
289
+ before: opts.before ?? null,
290
+ });
291
+ return data.searchIssues;
292
+ },
293
+ });
294
+ rl.registerAction("issue.addLabel", {
295
+ description: "Add a single label to an issue.",
296
+ inputSchema: {
297
+ issueId: { type: "string", required: true },
298
+ labelId: { type: "string", required: true },
299
+ },
300
+ async execute(input, ctx) {
301
+ const { issueId, labelId } = input;
302
+ const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueAddLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
303
+ return data.issueAddLabel;
304
+ },
305
+ });
306
+ rl.registerAction("issue.removeLabel", {
307
+ description: "Remove a single label from an issue.",
308
+ inputSchema: {
309
+ issueId: { type: "string", required: true },
310
+ labelId: { type: "string", required: true },
311
+ },
312
+ async execute(input, ctx) {
313
+ const { issueId, labelId } = input;
314
+ const data = await gql(key(ctx), `mutation($id: String!, $labelId: String!) { issueRemoveLabel(id: $id, labelId: $labelId) { success } }`, { id: issueId, labelId });
315
+ return data.issueRemoveLabel;
316
+ },
317
+ });
318
+ rl.registerAction("issue.subscribe", {
319
+ description: "Subscribe a user to issue notifications (defaults to current user).",
320
+ inputSchema: {
321
+ issueId: { type: "string", required: true },
322
+ userId: { type: "string", required: false },
323
+ userEmail: { type: "string", required: false },
324
+ },
325
+ async execute(input, ctx) {
326
+ const { issueId, userId, userEmail } = input;
327
+ const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
328
+ issueSubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
329
+ }`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
330
+ return data.issueSubscribe;
331
+ },
332
+ });
333
+ rl.registerAction("issue.unsubscribe", {
334
+ description: "Unsubscribe a user from issue notifications (defaults to current user).",
335
+ inputSchema: {
336
+ issueId: { type: "string", required: true },
337
+ userId: { type: "string", required: false },
338
+ userEmail: { type: "string", required: false },
339
+ },
340
+ async execute(input, ctx) {
341
+ const { issueId, userId, userEmail } = input;
342
+ const data = await gql(key(ctx), `mutation($id: String!, $userId: String, $userEmail: String) {
343
+ issueUnsubscribe(id: $id, userId: $userId, userEmail: $userEmail) { success }
344
+ }`, { id: issueId, userId: userId ?? null, userEmail: userEmail ?? null });
345
+ return data.issueUnsubscribe;
173
346
  },
174
347
  });
175
348
  rl.registerAction("issue.addLink", {
176
- description: "Add a link/relation between issues",
349
+ description: "Create a relation between two issues.",
177
350
  inputSchema: {
178
- issueId: {
179
- type: "string",
180
- required: true,
181
- description: "Source issue ID",
182
- },
183
- relatedIssueId: {
184
- type: "string",
185
- required: true,
186
- description: "Related issue ID",
187
- },
188
- type: {
189
- type: "string",
190
- required: true,
191
- description: "Relation type: relates, blocks, duplicate",
192
- },
351
+ issueId: { type: "string", required: true, description: "The identifier of the issue that is related to another issue. UUID or issue identifier (e.g., 'LIN-123')" },
352
+ relatedIssueId: { type: "string", required: true, description: "The identifier of the related issue. UUID or issue identifier (e.g., 'LIN-123')" },
353
+ type: { type: "string", required: true, description: "IssueRelationType: blocks | duplicate | related | similar" },
193
354
  },
194
355
  async execute(input, ctx) {
195
- const { issueId, relatedIssueId, type } = input;
196
- const data = await gql(key(ctx), `mutation($input: IssueRelationCreateInput!) { issueRelationCreate(input: $input) { success } }`, { input: { issueId, relatedIssueId, type } });
356
+ const data = await gql(key(ctx), `mutation($input: IssueRelationCreateInput!) { issueRelationCreate(input: $input) { success issueRelation { id type } } }`, { input: input });
197
357
  return data.issueRelationCreate;
198
358
  },
199
359
  });
360
+ rl.registerAction("issue.listComments", {
361
+ description: "List comments on an issue.",
362
+ inputSchema: {
363
+ issueId: { type: "string", required: true },
364
+ limit: { type: "number", required: false },
365
+ },
366
+ async execute(input, ctx) {
367
+ const { issueId, limit } = input;
368
+ const data = await gql(key(ctx), `query($id: String!, $first: Int) {
369
+ issue(id: $id) { comments(first: $first) { nodes { ${COMMENT_FIELDS} } } }
370
+ }`, { id: issueId, first: limit ?? 50 });
371
+ return data.issue?.comments?.nodes;
372
+ },
373
+ });
374
+ // =========================================================
375
+ // Comments
376
+ // =========================================================
377
+ rl.registerAction("issue.addComment", {
378
+ description: "Add a comment to an issue. Pass parentId to nest as a reply.",
379
+ inputSchema: {
380
+ issueId: { type: "string", required: true, description: "The issue to associate the comment with. UUID or issue identifier (e.g., 'LIN-123')" },
381
+ body: { type: "string", required: true, description: "The comment content in markdown format" },
382
+ parentId: { type: "string", required: false, description: "The parent comment under which to nest this comment" },
383
+ doNotSubscribeToIssue: { type: "boolean", required: false, description: "Prevent auto-subscription to the issue the comment is created on" },
384
+ quotedText: { type: "string", required: false, description: "The text that this comment references (inline comments)" },
385
+ },
386
+ async execute(input, ctx) {
387
+ const fields = input;
388
+ const data = await gql(key(ctx), `mutation($input: CommentCreateInput!) { commentCreate(input: $input) { success comment { ${COMMENT_FIELDS} } } }`, { input: fields });
389
+ return data.commentCreate?.comment;
390
+ },
391
+ });
392
+ listAction("comment.list", "List comments across the workspace.", "comments", "CommentFilter", COMMENT_FIELDS);
393
+ rl.registerAction("comment.get", {
394
+ description: "Get a comment by ID.",
395
+ inputSchema: { id: { type: "string", required: true } },
396
+ async execute(input, ctx) {
397
+ const data = await gql(key(ctx), `query($id: String!) { comment(id: $id) { ${COMMENT_FIELDS} } }`, { id: input.id });
398
+ return data.comment;
399
+ },
400
+ });
401
+ rl.registerAction("comment.update", {
402
+ description: "Update a comment.",
403
+ inputSchema: {
404
+ id: { type: "string", required: true, description: "The identifier of the comment to update" },
405
+ body: { type: "string", required: false, description: "The comment content in markdown format" },
406
+ quotedText: { type: "string", required: false, description: "The text that this comment references (inline comments)" },
407
+ },
408
+ async execute(input, ctx) {
409
+ const { id, ...fields } = input;
410
+ const data = await gql(key(ctx), `mutation($id: String!, $input: CommentUpdateInput!) { commentUpdate(id: $id, input: $input) { success comment { ${COMMENT_FIELDS} } } }`, { id, input: fields });
411
+ return data.commentUpdate?.comment;
412
+ },
413
+ });
414
+ rl.registerAction("comment.delete", {
415
+ description: "Delete a comment.",
416
+ inputSchema: { id: { type: "string", required: true } },
417
+ async execute(input, ctx) {
418
+ const data = await gql(key(ctx), `mutation($id: String!) { commentDelete(id: $id) { success } }`, { id: input.id });
419
+ return data.commentDelete;
420
+ },
421
+ });
422
+ // =========================================================
423
+ // Workflow States
424
+ // =========================================================
425
+ listAction("state.list", "List workflow states. Filter by team for team-scoped states.", "workflowStates", "WorkflowStateFilter", STATE_FIELDS);
426
+ getAction("state.get", "Get a workflow state by ID.", "workflowState", STATE_FIELDS);
427
+ rl.registerAction("state.create", {
428
+ description: "Create a workflow state in a team.",
429
+ inputSchema: {
430
+ teamId: { type: "string", required: true, description: "The team associated with the state" },
431
+ name: { type: "string", required: true, description: "The name of the state" },
432
+ type: { type: "string", required: true, description: "The workflow state type which categorizes the state. Valid values: backlog, unstarted, started, completed, canceled" },
433
+ color: { type: "string", required: true, description: "The color of the state (hex, e.g. #6B7280)" },
434
+ description: { type: "string", required: false, description: "The description of the state" },
435
+ position: { type: "number", required: false, description: "The position of the state (Float)" },
436
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
437
+ },
438
+ async execute(input, ctx) {
439
+ const data = await gql(key(ctx), `mutation($input: WorkflowStateCreateInput!) { workflowStateCreate(input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { input: input });
440
+ return data.workflowStateCreate?.workflowState;
441
+ },
442
+ });
443
+ rl.registerAction("state.update", {
444
+ description: "Update a workflow state. Type cannot be changed after creation.",
445
+ inputSchema: {
446
+ id: { type: "string", required: true, description: "The identifier of the state to update" },
447
+ name: { type: "string", required: false, description: "The name of the state" },
448
+ color: { type: "string", required: false, description: "The color of the state (hex)" },
449
+ description: { type: "string", required: false, description: "The description of the state" },
450
+ position: { type: "number", required: false, description: "The position of the state (Float)" },
451
+ },
452
+ async execute(input, ctx) {
453
+ const { id, ...fields } = input;
454
+ const data = await gql(key(ctx), `mutation($id: String!, $input: WorkflowStateUpdateInput!) { workflowStateUpdate(id: $id, input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { id, input: fields });
455
+ return data.workflowStateUpdate?.workflowState;
456
+ },
457
+ });
458
+ // =========================================================
459
+ // Labels
460
+ // =========================================================
461
+ listAction("label.list", "List labels (workspace + team-scoped).", "issueLabels", "IssueLabelFilter", LABEL_FIELDS);
462
+ getAction("label.get", "Get a label by ID.", "issueLabel", LABEL_FIELDS);
463
+ rl.registerAction("label.create", {
464
+ description: "Create a label. Omit teamId for a workspace-level label.",
465
+ inputSchema: {
466
+ name: { type: "string", required: true, description: "The name of the label" },
467
+ teamId: { type: "string", required: false, description: "The team associated with the label. If omitted, the label is workspace-scoped" },
468
+ color: { type: "string", required: false, description: "The color of the label (hex)" },
469
+ description: { type: "string", required: false, description: "The description of the label" },
470
+ parentId: { type: "string", required: false, description: "The identifier of the parent label (group label)" },
471
+ isGroup: { type: "boolean", required: false, description: "Whether the label is a group" },
472
+ retiredAt: { type: "string", required: false, description: "The time at which the label was retired (DateTime). Set to null to restore a retired label" },
473
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
474
+ replaceTeamLabels: { type: "boolean", required: false, description: "Replace all team-specific labels with the same name with this newly created workspace label (default false)" },
475
+ },
476
+ async execute(input, ctx) {
477
+ const { replaceTeamLabels, ...fields } = input;
478
+ 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 });
479
+ return data.issueLabelCreate?.issueLabel;
480
+ },
481
+ });
482
+ rl.registerAction("label.update", {
483
+ description: "Update a label.",
484
+ inputSchema: {
485
+ id: { type: "string", required: true, description: "The identifier of the label to update" },
486
+ name: { type: "string", required: false, description: "The name of the label" },
487
+ color: { type: "string", required: false, description: "The color of the label (hex)" },
488
+ description: { type: "string", required: false, description: "The description of the label" },
489
+ parentId: { type: "string", required: false, description: "The identifier of the parent label" },
490
+ isGroup: { type: "boolean", required: false, description: "Whether the label is a group" },
491
+ retiredAt: { type: "string", required: false, description: "The time at which the label was retired (DateTime). Set to null to restore a retired label" },
492
+ replaceTeamLabels: { type: "boolean", required: false, description: "Replace all team-specific labels with the same name with this updated workspace label (default false)" },
493
+ },
494
+ async execute(input, ctx) {
495
+ const { id, replaceTeamLabels, ...fields } = input;
496
+ 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 });
497
+ return data.issueLabelUpdate?.issueLabel;
498
+ },
499
+ });
500
+ rl.registerAction("label.delete", {
501
+ description: "Delete a label.",
502
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the label to delete" } },
503
+ async execute(input, ctx) {
504
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelDelete(id: $id) { success } }`, { id: input.id });
505
+ return data.issueLabelDelete;
506
+ },
507
+ });
508
+ rl.registerAction("label.retire", {
509
+ description: "Retire a label. Retired labels remain visible but cannot be applied to new issues.",
510
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the label to retire" } },
511
+ async execute(input, ctx) {
512
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRetire(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
513
+ return data.issueLabelRetire?.issueLabel;
514
+ },
515
+ });
516
+ rl.registerAction("label.restore", {
517
+ description: "Restore a previously retired label.",
518
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the label to restore" } },
519
+ async execute(input, ctx) {
520
+ const data = await gql(key(ctx), `mutation($id: String!) { issueLabelRestore(id: $id) { success issueLabel { ${LABEL_FIELDS} } } }`, { id: input.id });
521
+ return data.issueLabelRestore?.issueLabel;
522
+ },
523
+ });
524
+ // =========================================================
525
+ // Projects
526
+ // =========================================================
527
+ listAction("project.list", "List projects.", "projects", "ProjectFilter", PROJECT_FIELDS);
528
+ getAction("project.get", "Get a project by ID or slug.", "project", PROJECT_FIELDS);
529
+ rl.registerAction("project.create", {
530
+ description: "Create a project. teamIds is required.",
531
+ inputSchema: {
532
+ name: { type: "string", required: true, description: "The name of the project" },
533
+ teamIds: { type: "array", required: true, description: "The identifiers of the teams this project is associated with" },
534
+ description: { type: "string", required: false, description: "The description for the project" },
535
+ content: { type: "string", required: false, description: "The project content as markdown" },
536
+ icon: { type: "string", required: false, description: "The icon of the project" },
537
+ color: { type: "string", required: false, description: "The color of the project (hex)" },
538
+ priority: { type: "number", required: false, description: "The priority of the project. 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low" },
539
+ leadId: { type: "string", required: false, description: "The identifier of the project lead" },
540
+ memberIds: { type: "array", required: false, description: "The identifiers of the members of this project" },
541
+ startDate: { type: "string", required: false, description: "The planned start date of the project (TimelessDate, YYYY-MM-DD)" },
542
+ startDateResolution: { type: "string", required: false, description: "The resolution of the project's start date (DateResolutionType)" },
543
+ targetDate: { type: "string", required: false, description: "The planned target date of the project (TimelessDate, YYYY-MM-DD)" },
544
+ targetDateResolution: { type: "string", required: false, description: "The resolution of the project's estimated completion date (DateResolutionType)" },
545
+ statusId: { type: "string", required: false, description: "The ID of the project status" },
546
+ labelIds: { type: "array", required: false, description: "The identifiers of the project labels associated with this project" },
547
+ sortOrder: { type: "number", required: false, description: "The sort order for the project in shared views (Float)" },
548
+ templateId: { type: "string", required: false, description: "The ID of a project template to apply when creating the project" },
549
+ useDefaultTemplate: { type: "boolean", required: false, description: "Apply the default project template of the first team provided. Ignored if templateId is set" },
550
+ convertedFromIssueId: { type: "string", required: false, description: "The ID of the issue that was converted into this project" },
551
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
552
+ slackChannelName: { type: "string", required: false, description: "The full name for the Slack channel to create (including prefix). Creates and connects a Slack channel if provided" },
553
+ },
554
+ async execute(input, ctx) {
555
+ const { slackChannelName, ...fields } = input;
556
+ 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 });
557
+ return data.projectCreate?.project;
558
+ },
559
+ });
560
+ rl.registerAction("project.update", {
561
+ description: "Update a project.",
562
+ inputSchema: {
563
+ id: { type: "string", required: true, description: "The identifier of the project to update (UUID or slug)" },
564
+ name: { type: "string", required: false, description: "The name of the project" },
565
+ description: { type: "string", required: false, description: "The description for the project" },
566
+ content: { type: "string", required: false, description: "The project content as markdown" },
567
+ icon: { type: "string", required: false, description: "The icon of the project" },
568
+ color: { type: "string", required: false, description: "The color of the project (hex)" },
569
+ priority: { type: "number", required: false, description: "The priority of the project. 0=No, 1=Urgent, 2=High, 3=Medium, 4=Low" },
570
+ leadId: { type: "string", required: false, description: "The identifier of the project lead" },
571
+ memberIds: { type: "array", required: false, description: "The identifiers of the members of this project" },
572
+ startDate: { type: "string", required: false, description: "The planned start date (TimelessDate, YYYY-MM-DD)" },
573
+ startDateResolution: { type: "string", required: false, description: "The resolution of the project's start date (DateResolutionType)" },
574
+ targetDate: { type: "string", required: false, description: "The planned target date (TimelessDate, YYYY-MM-DD)" },
575
+ targetDateResolution: { type: "string", required: false, description: "The resolution of the project's estimated completion date (DateResolutionType)" },
576
+ statusId: { type: "string", required: false, description: "The ID of the project status" },
577
+ labelIds: { type: "array", required: false, description: "The identifiers of the project labels associated with this project" },
578
+ teamIds: { type: "array", required: false, description: "The identifiers of the teams this project is associated with" },
579
+ sortOrder: { type: "number", required: false, description: "The sort order for the project in shared views (Float)" },
580
+ completedAt: { type: "string", required: false, description: "The time at which the project was completed (DateTime)" },
581
+ canceledAt: { type: "string", required: false, description: "The time at which the project was canceled (DateTime)" },
582
+ trashed: { type: "boolean", required: false, description: "Whether the project has been trashed. Set to true to trash, or null to restore" },
583
+ },
584
+ async execute(input, ctx) {
585
+ const { id, ...fields } = input;
586
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateInput!) { projectUpdate(id: $id, input: $input) { success project { ${PROJECT_FIELDS} } } }`, { id, input: fields });
587
+ return data.projectUpdate?.project;
588
+ },
589
+ });
590
+ rl.registerAction("project.delete", {
591
+ description: "Trash (soft-delete) a project. Restorable via project.unarchive.",
592
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the project to delete" } },
593
+ async execute(input, ctx) {
594
+ const data = await gql(key(ctx), `mutation($id: String!) { projectDelete(id: $id) { success } }`, { id: input.id });
595
+ return data.projectDelete;
596
+ },
597
+ });
598
+ rl.registerAction("project.unarchive", {
599
+ description: "Restore a previously trashed or archived project.",
600
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the project to restore (UUID or slug)" } },
601
+ async execute(input, ctx) {
602
+ const data = await gql(key(ctx), `mutation($id: String!) { projectUnarchive(id: $id) { success } }`, { id: input.id });
603
+ return data.projectUnarchive;
604
+ },
605
+ });
606
+ rl.registerAction("project.search", {
607
+ description: "Search projects by text. Rate-limited to 30 req/min.",
608
+ inputSchema: {
609
+ term: { type: "string", required: true, description: "Search string to look for" },
610
+ limit: { type: "number", required: false, description: "Max results (forward pagination, default 50)" },
611
+ includeComments: { type: "boolean", required: false, description: "Should associated comments be searched (default false)" },
612
+ teamId: { type: "string", required: false, description: "UUID of a team to boost in search results" },
613
+ },
614
+ async execute(input, ctx) {
615
+ const opts = input;
616
+ const data = await gql(key(ctx), `query($term: String!, $first: Int, $includeComments: Boolean, $teamId: String) {
617
+ searchProjects(term: $term, first: $first, includeComments: $includeComments, teamId: $teamId) {
618
+ nodes { ${PROJECT_FIELDS} }
619
+ totalCount
620
+ }
621
+ }`, {
622
+ term: opts.term,
623
+ first: opts.limit ?? 50,
624
+ includeComments: opts.includeComments ?? null,
625
+ teamId: opts.teamId ?? null,
626
+ });
627
+ return data.searchProjects;
628
+ },
629
+ });
630
+ // =========================================================
631
+ // Project Milestones
632
+ // =========================================================
633
+ listAction("milestone.list", "List project milestones.", "projectMilestones", "ProjectMilestoneFilter", MILESTONE_FIELDS);
634
+ getAction("milestone.get", "Get a project milestone by ID.", "projectMilestone", MILESTONE_FIELDS);
635
+ rl.registerAction("milestone.create", {
636
+ description: "Create a project milestone.",
637
+ inputSchema: {
638
+ projectId: { type: "string", required: true, description: "Related project for the project milestone" },
639
+ name: { type: "string", required: true, description: "The name of the project milestone" },
640
+ description: { type: "string", required: false, description: "The description of the project milestone in markdown format" },
641
+ targetDate: { type: "string", required: false, description: "The planned target date of the project milestone (TimelessDate, YYYY-MM-DD)" },
642
+ sortOrder: { type: "number", required: false, description: "The sort order for the project milestone within a project (Float)" },
643
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
644
+ },
645
+ async execute(input, ctx) {
646
+ const data = await gql(key(ctx), `mutation($input: ProjectMilestoneCreateInput!) { projectMilestoneCreate(input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { input: input });
647
+ return data.projectMilestoneCreate?.projectMilestone;
648
+ },
649
+ });
650
+ rl.registerAction("milestone.update", {
651
+ description: "Update a project milestone.",
652
+ inputSchema: {
653
+ id: { type: "string", required: true, description: "The identifier of the project milestone to update" },
654
+ name: { type: "string", required: false, description: "The name of the project milestone" },
655
+ description: { type: "string", required: false, description: "The description of the project milestone in markdown format" },
656
+ targetDate: { type: "string", required: false, description: "The planned target date (TimelessDate, YYYY-MM-DD)" },
657
+ projectId: { type: "string", required: false, description: "Related project for the project milestone (move to another project)" },
658
+ sortOrder: { type: "number", required: false, description: "The sort order for the project milestone within a project (Float)" },
659
+ },
660
+ async execute(input, ctx) {
661
+ const { id, ...fields } = input;
662
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectMilestoneUpdateInput!) { projectMilestoneUpdate(id: $id, input: $input) { success projectMilestone { ${MILESTONE_FIELDS} } } }`, { id, input: fields });
663
+ return data.projectMilestoneUpdate?.projectMilestone;
664
+ },
665
+ });
666
+ rl.registerAction("milestone.delete", {
667
+ description: "Delete a project milestone.",
668
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the project milestone to delete" } },
669
+ async execute(input, ctx) {
670
+ const data = await gql(key(ctx), `mutation($id: String!) { projectMilestoneDelete(id: $id) { success } }`, { id: input.id });
671
+ return data.projectMilestoneDelete;
672
+ },
673
+ });
674
+ // =========================================================
675
+ // Project Updates (status posts)
676
+ // =========================================================
677
+ listAction("projectUpdate.list", "List project updates.", "projectUpdates", "ProjectUpdateFilter", PROJECT_UPDATE_FIELDS);
678
+ rl.registerAction("projectUpdate.create", {
679
+ description: "Post a status update on a project.",
680
+ inputSchema: {
681
+ projectId: { type: "string", required: true, description: "The project to associate the project update with" },
682
+ body: { type: "string", required: false, description: "The content of the project update in markdown format" },
683
+ health: { type: "string", required: false, description: "The health of the project at the time of the update (ProjectUpdateHealthType: onTrack | atRisk | offTrack)" },
684
+ isDiffHidden: { type: "boolean", required: false, description: "Whether the diff between the current update and the previous one should be hidden" },
685
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
686
+ },
687
+ async execute(input, ctx) {
688
+ const data = await gql(key(ctx), `mutation($input: ProjectUpdateCreateInput!) { projectUpdateCreate(input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { input: input });
689
+ return data.projectUpdateCreate?.projectUpdate;
690
+ },
691
+ });
692
+ rl.registerAction("projectUpdate.update", {
693
+ description: "Update a project status update.",
694
+ inputSchema: {
695
+ id: { type: "string", required: true, description: "The identifier of the project update to update" },
696
+ body: { type: "string", required: false, description: "The content of the project update in markdown format" },
697
+ health: { type: "string", required: false, description: "The health of the project at the time of the update (ProjectUpdateHealthType: onTrack | atRisk | offTrack)" },
698
+ isDiffHidden: { type: "boolean", required: false, description: "Whether the diff between the current update and the previous one should be hidden" },
699
+ },
700
+ async execute(input, ctx) {
701
+ const { id, ...fields } = input;
702
+ const data = await gql(key(ctx), `mutation($id: String!, $input: ProjectUpdateUpdateInput!) { projectUpdateUpdate(id: $id, input: $input) { success projectUpdate { ${PROJECT_UPDATE_FIELDS} } } }`, { id, input: fields });
703
+ return data.projectUpdateUpdate?.projectUpdate;
704
+ },
705
+ });
706
+ rl.registerAction("projectUpdate.archive", {
707
+ description: "Archive a project status update.",
708
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the project update to archive" } },
709
+ async execute(input, ctx) {
710
+ const data = await gql(key(ctx), `mutation($id: String!) { projectUpdateArchive(id: $id) { success } }`, { id: input.id });
711
+ return data.projectUpdateArchive;
712
+ },
713
+ });
714
+ // =========================================================
715
+ // Cycles
716
+ // =========================================================
717
+ listAction("cycle.list", "List cycles. Use filter for isActive/isNext/isPrevious.", "cycles", "CycleFilter", CYCLE_FIELDS);
718
+ getAction("cycle.get", "Get a cycle by ID.", "cycle", CYCLE_FIELDS);
719
+ rl.registerAction("cycle.create", {
720
+ description: "Create a cycle for a team.",
721
+ inputSchema: {
722
+ teamId: { type: "string", required: true, description: "The team to associate the cycle with" },
723
+ startsAt: { type: "string", required: true, description: "The start time of the cycle (DateTime, ISO 8601)" },
724
+ endsAt: { type: "string", required: true, description: "The end time of the cycle (DateTime, ISO 8601)" },
725
+ name: { type: "string", required: false, description: "The custom name of the cycle" },
726
+ description: { type: "string", required: false, description: "The description of the cycle" },
727
+ completedAt: { type: "string", required: false, description: "The completion time of the cycle (DateTime). If null, the cycle hasn't been completed" },
728
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
729
+ },
730
+ async execute(input, ctx) {
731
+ const data = await gql(key(ctx), `mutation($input: CycleCreateInput!) { cycleCreate(input: $input) { success cycle { ${CYCLE_FIELDS} } } }`, { input: input });
732
+ return data.cycleCreate?.cycle;
733
+ },
734
+ });
735
+ rl.registerAction("cycle.update", {
736
+ description: "Update a cycle.",
737
+ inputSchema: {
738
+ id: { type: "string", required: true, description: "The identifier of the cycle to update" },
739
+ name: { type: "string", required: false, description: "The custom name of the cycle" },
740
+ description: { type: "string", required: false, description: "The description of the cycle" },
741
+ startsAt: { type: "string", required: false, description: "The start time of the cycle (DateTime, ISO 8601)" },
742
+ endsAt: { type: "string", required: false, description: "The end time of the cycle (DateTime, ISO 8601)" },
743
+ completedAt: { type: "string", required: false, description: "The completion time of the cycle (DateTime). If null, the cycle hasn't been completed" },
744
+ },
745
+ async execute(input, ctx) {
746
+ const { id, ...fields } = input;
747
+ const data = await gql(key(ctx), `mutation($id: String!, $input: CycleUpdateInput!) { cycleUpdate(id: $id, input: $input) { success cycle { ${CYCLE_FIELDS} } } }`, { id, input: fields });
748
+ return data.cycleUpdate?.cycle;
749
+ },
750
+ });
751
+ // =========================================================
752
+ // Initiatives
753
+ // =========================================================
754
+ listAction("initiative.list", "List initiatives.", "initiatives", "InitiativeFilter", INITIATIVE_FIELDS);
755
+ getAction("initiative.get", "Get an initiative by ID or slug.", "initiative", INITIATIVE_FIELDS);
756
+ rl.registerAction("initiative.create", {
757
+ description: "Create an initiative. Status: Planned | Active | Completed.",
758
+ inputSchema: {
759
+ name: { type: "string", required: true, description: "The name of the initiative" },
760
+ description: { type: "string", required: false, description: "The description of the initiative" },
761
+ content: { type: "string", required: false, description: "The initiative's content in markdown format" },
762
+ icon: { type: "string", required: false, description: "The initiative's icon" },
763
+ color: { type: "string", required: false, description: "The initiative's color (hex)" },
764
+ ownerId: { type: "string", required: false, description: "The owner of the initiative" },
765
+ status: { type: "string", required: false, description: "The initiative's status (InitiativeStatus: Planned | Active | Completed)" },
766
+ targetDate: { type: "string", required: false, description: "The estimated completion date of the initiative (TimelessDate, YYYY-MM-DD)" },
767
+ targetDateResolution: { type: "string", required: false, description: "The resolution of the initiative's estimated completion date (DateResolutionType)" },
768
+ sortOrder: { type: "number", required: false, description: "The sort order of the initiative within the workspace (Float)" },
769
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
770
+ },
771
+ async execute(input, ctx) {
772
+ const data = await gql(key(ctx), `mutation($input: InitiativeCreateInput!) { initiativeCreate(input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { input: input });
773
+ return data.initiativeCreate?.initiative;
774
+ },
775
+ });
776
+ rl.registerAction("initiative.update", {
777
+ description: "Update an initiative.",
778
+ inputSchema: {
779
+ id: { type: "string", required: true, description: "The identifier of the initiative to update" },
780
+ name: { type: "string", required: false, description: "The name of the initiative" },
781
+ description: { type: "string", required: false, description: "The description of the initiative" },
782
+ content: { type: "string", required: false, description: "The initiative's content in markdown format" },
783
+ icon: { type: "string", required: false, description: "The initiative's icon" },
784
+ color: { type: "string", required: false, description: "The initiative's color (hex)" },
785
+ ownerId: { type: "string", required: false, description: "The owner of the initiative" },
786
+ status: { type: "string", required: false, description: "The initiative's status (InitiativeStatus: Planned | Active | Completed)" },
787
+ targetDate: { type: "string", required: false, description: "The estimated completion date (TimelessDate, YYYY-MM-DD). Set to null to clear" },
788
+ targetDateResolution: { type: "string", required: false, description: "The resolution of the initiative's estimated completion date (DateResolutionType)" },
789
+ sortOrder: { type: "number", required: false, description: "The sort order of the initiative within the workspace (Float)" },
790
+ trashed: { type: "boolean", required: false, description: "Whether the initiative has been trashed. Set to true to trash, or null to restore" },
791
+ },
792
+ async execute(input, ctx) {
793
+ const { id, ...fields } = input;
794
+ const data = await gql(key(ctx), `mutation($id: String!, $input: InitiativeUpdateInput!) { initiativeUpdate(id: $id, input: $input) { success initiative { ${INITIATIVE_FIELDS} } } }`, { id, input: fields });
795
+ return data.initiativeUpdate?.initiative;
796
+ },
797
+ });
798
+ rl.registerAction("initiative.delete", {
799
+ description: "Trash an initiative.",
800
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the initiative to delete" } },
801
+ async execute(input, ctx) {
802
+ const data = await gql(key(ctx), `mutation($id: String!) { initiativeDelete(id: $id) { success } }`, { id: input.id });
803
+ return data.initiativeDelete;
804
+ },
805
+ });
806
+ rl.registerAction("initiative.addProject", {
807
+ description: "Associate a project with an initiative. A project can only appear once in an initiative hierarchy.",
808
+ inputSchema: {
809
+ initiativeId: { type: "string", required: true, description: "The identifier of the initiative" },
810
+ projectId: { type: "string", required: true, description: "The identifier of the project" },
811
+ sortOrder: { type: "number", required: false, description: "The sort order for the project within the initiative (Float)" },
812
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
813
+ },
814
+ async execute(input, ctx) {
815
+ const data = await gql(key(ctx), `mutation($input: InitiativeToProjectCreateInput!) { initiativeToProjectCreate(input: $input) { success initiativeToProject { id } } }`, { input: input });
816
+ return data.initiativeToProjectCreate;
817
+ },
818
+ });
819
+ rl.registerAction("initiative.removeProject", {
820
+ description: "Remove a project from an initiative. Pass the link id returned by initiative.addProject.",
821
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the initiativeToProject to delete" } },
822
+ async execute(input, ctx) {
823
+ const data = await gql(key(ctx), `mutation($id: String!) { initiativeToProjectDelete(id: $id) { success } }`, { id: input.id });
824
+ return data.initiativeToProjectDelete;
825
+ },
826
+ });
827
+ // =========================================================
828
+ // Teams
829
+ // =========================================================
830
+ listAction("team.list", "List teams whose issues you can access.", "teams", "TeamFilter", TEAM_FIELDS);
831
+ getAction("team.get", "Get a team by ID or key.", "team", TEAM_FIELDS);
832
+ rl.registerAction("team.create", {
833
+ description: "Create a team. Most settings have sensible defaults.",
834
+ inputSchema: {
835
+ name: { type: "string", required: true, description: "The name of the team" },
836
+ key: { type: "string", required: false, description: "The key of the team. If not given, the key will be generated based on the name" },
837
+ description: { type: "string", required: false, description: "The description of the team" },
838
+ icon: { type: "string", required: false, description: "The icon of the team" },
839
+ color: { type: "string", required: false, description: "The color of the team (hex)" },
840
+ private: { type: "boolean", required: false, description: "Whether the team is private" },
841
+ timezone: { type: "string", required: false, description: "The timezone of the team" },
842
+ cyclesEnabled: { type: "boolean", required: false, description: "Whether the team uses cycles" },
843
+ cycleDuration: { type: "number", required: false, description: "The duration of each cycle in weeks (Int)" },
844
+ cycleCooldownTime: { type: "number", required: false, description: "The cooldown time after each cycle in weeks (Int)" },
845
+ cycleStartDay: { type: "number", required: false, description: "The day of the week that a new cycle starts. 0=Sun..6=Sat (Float)" },
846
+ upcomingCycleCount: { type: "number", required: false, description: "How many upcoming cycles to create (Float)" },
847
+ issueEstimationType: { type: "string", required: false, description: "The issue estimation type: notUsed | exponential | fibonacci | linear | tShirt" },
848
+ triageEnabled: { type: "boolean", required: false, description: "Whether triage mode is enabled for the team" },
849
+ parentId: { type: "string", required: false, description: "The parent team ID (for sub-teams)" },
850
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
851
+ copySettingsFromTeamId: { type: "string", required: false, description: "The team id to copy settings from, if any" },
852
+ },
853
+ async execute(input, ctx) {
854
+ const { copySettingsFromTeamId, ...fields } = input;
855
+ 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 });
856
+ return data.teamCreate?.team;
857
+ },
858
+ });
859
+ rl.registerAction("team.update", {
860
+ description: "Update a team. Requires team owner or workspace admin permissions.",
861
+ inputSchema: {
862
+ id: { type: "string", required: true, description: "The identifier of the team to update" },
863
+ name: { type: "string", required: false, description: "The name of the team" },
864
+ key: { type: "string", required: false, description: "The key of the team" },
865
+ description: { type: "string", required: false, description: "The description of the team" },
866
+ icon: { type: "string", required: false, description: "The icon of the team" },
867
+ color: { type: "string", required: false, description: "The color of the team (hex)" },
868
+ private: { type: "boolean", required: false, description: "Whether the team is private" },
869
+ timezone: { type: "string", required: false, description: "The timezone of the team" },
870
+ cyclesEnabled: { type: "boolean", required: false, description: "Whether the team uses cycles" },
871
+ cycleDuration: { type: "number", required: false, description: "The duration of each cycle in weeks (Int)" },
872
+ cycleCooldownTime: { type: "number", required: false, description: "The cooldown time after each cycle in weeks (Int)" },
873
+ cycleStartDay: { type: "number", required: false, description: "The day of the week that a new cycle starts. 0=Sun..6=Sat (Float)" },
874
+ upcomingCycleCount: { type: "number", required: false, description: "How many upcoming cycles to create (Float)" },
875
+ issueEstimationType: { type: "string", required: false, description: "The issue estimation type: notUsed | exponential | fibonacci | linear | tShirt" },
876
+ triageEnabled: { type: "boolean", required: false, description: "Whether triage mode is enabled for the team" },
877
+ },
878
+ async execute(input, ctx) {
879
+ const { id, ...fields } = input;
880
+ const data = await gql(key(ctx), `mutation($id: String!, $input: TeamUpdateInput!) { teamUpdate(id: $id, input: $input) { success team { ${TEAM_FIELDS} } } }`, { id, input: fields });
881
+ return data.teamUpdate?.team;
882
+ },
883
+ });
884
+ rl.registerAction("team.members", {
885
+ description: "List members of a team.",
886
+ inputSchema: {
887
+ teamId: { type: "string", required: true, description: "The identifier of the team" },
888
+ limit: { type: "number", required: false, description: "Max members to return (default 50)" },
889
+ },
890
+ async execute(input, ctx) {
891
+ const { teamId, limit } = input;
892
+ const data = await gql(key(ctx), `query($id: String!, $first: Int) {
893
+ team(id: $id) { members(first: $first) { nodes { ${USER_FIELDS} } } }
894
+ }`, { id: teamId, first: limit ?? 50 });
895
+ return data.team?.members?.nodes;
896
+ },
897
+ });
898
+ // =========================================================
899
+ // Users
900
+ // =========================================================
901
+ listAction("user.list", "List users in the workspace.", "users", "UserFilter", USER_FIELDS);
902
+ getAction("user.get", "Get a user by ID. Use 'me' to reference the authenticated user.", "user", USER_FIELDS);
903
+ rl.registerAction("user.me", {
904
+ description: "Get the authenticated user.",
905
+ inputSchema: {},
906
+ async execute(_input, ctx) {
907
+ const data = await gql(key(ctx), `query { viewer { ${USER_FIELDS} } }`);
908
+ return data.viewer;
909
+ },
910
+ });
911
+ rl.registerAction("user.update", {
912
+ description: "Update a user. Use id='me' to update the authenticated user.",
913
+ inputSchema: {
914
+ id: { type: "string", required: true, description: "The identifier of the user to update. Use 'me' to reference the currently authenticated user" },
915
+ name: { type: "string", required: false, description: "The name of the user" },
916
+ displayName: { type: "string", required: false, description: "The display name of the user" },
917
+ description: { type: "string", required: false, description: "The user description or short bio" },
918
+ avatarUrl: { type: "string", required: false, description: "The avatar image URL of the user" },
919
+ timezone: { type: "string", required: false, description: "The local timezone of the user" },
920
+ title: { type: "string", required: false, description: "The user's job title" },
921
+ statusEmoji: { type: "string", required: false, description: "The emoji part of the user status" },
922
+ statusLabel: { type: "string", required: false, description: "The label part of the user status" },
923
+ statusUntilAt: { type: "string", required: false, description: "When the user status should be cleared (DateTime)" },
924
+ },
925
+ async execute(input, ctx) {
926
+ const { id, ...fields } = input;
927
+ const data = await gql(key(ctx), `mutation($id: String!, $input: UserUpdateInput!) { userUpdate(id: $id, input: $input) { success user { ${USER_FIELDS} } } }`, { id, input: fields });
928
+ return data.userUpdate?.user;
929
+ },
930
+ });
931
+ // =========================================================
932
+ // Attachments
933
+ // =========================================================
934
+ listAction("attachment.list", "List issue attachments.", "attachments", "AttachmentFilter", ATTACHMENT_FIELDS);
935
+ getAction("attachment.get", "Get an attachment by ID.", "attachment", ATTACHMENT_FIELDS);
936
+ rl.registerAction("attachment.create", {
937
+ description: "Create an attachment on an issue.",
938
+ inputSchema: {
939
+ issueId: { type: "string", required: true, description: "The issue to associate the attachment with. UUID or issue identifier (e.g., 'LIN-123')" },
940
+ title: { type: "string", required: true, description: "The attachment title" },
941
+ url: { type: "string", required: true, description: "Attachment location, also used as a unique identifier. Re-creating with the same url updates the existing record" },
942
+ subtitle: { type: "string", required: false, description: "The attachment subtitle" },
943
+ iconUrl: { type: "string", required: false, description: "An icon url to display with the attachment (jpg or png, max 1MB, ideally 20x20px)" },
944
+ commentBody: { type: "string", required: false, description: "Create a linked comment with markdown body" },
945
+ groupBySource: { type: "boolean", required: false, description: "Whether attachments for the same source application should be grouped in the Linear UI" },
946
+ metadata: { type: "object", required: false, description: "Attachment metadata object with string and number values (JSONObject)" },
947
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
948
+ },
949
+ async execute(input, ctx) {
950
+ const data = await gql(key(ctx), `mutation($input: AttachmentCreateInput!) { attachmentCreate(input: $input) { success attachment { ${ATTACHMENT_FIELDS} } } }`, { input: input });
951
+ return data.attachmentCreate?.attachment;
952
+ },
953
+ });
954
+ rl.registerAction("attachment.update", {
955
+ description: "Update an attachment. title is required.",
956
+ inputSchema: {
957
+ id: { type: "string", required: true, description: "The identifier of the attachment to update" },
958
+ title: { type: "string", required: true, description: "The attachment title" },
959
+ subtitle: { type: "string", required: false, description: "The attachment subtitle" },
960
+ iconUrl: { type: "string", required: false, description: "An icon url to display with the attachment" },
961
+ metadata: { type: "object", required: false, description: "Attachment metadata object with string and number values (JSONObject)" },
962
+ },
963
+ async execute(input, ctx) {
964
+ const { id, ...fields } = input;
965
+ const data = await gql(key(ctx), `mutation($id: String!, $input: AttachmentUpdateInput!) { attachmentUpdate(id: $id, input: $input) { success attachment { ${ATTACHMENT_FIELDS} } } }`, { id, input: fields });
966
+ return data.attachmentUpdate?.attachment;
967
+ },
968
+ });
969
+ rl.registerAction("attachment.linkURL", {
970
+ description: "Link any URL to an issue. If a workspace integration matches the URL (Zendesk, GitHub, Slack, etc.) a rich attachment is created; otherwise a basic one.",
971
+ inputSchema: {
972
+ issueId: { type: "string", required: true, description: "The issue for which to link the url. UUID or issue identifier (e.g., 'LIN-123')" },
973
+ url: { type: "string", required: true, description: "The url to link" },
974
+ title: { type: "string", required: false, description: "The title to use for the attachment" },
975
+ id: { type: "string", required: false, description: "The id for the attachment (optional UUID override)" },
976
+ },
977
+ async execute(input, ctx) {
978
+ const { issueId, url, title, id } = input;
979
+ const data = await gql(key(ctx), `mutation($issueId: String!, $url: String!, $title: String, $id: String) {
980
+ attachmentLinkURL(issueId: $issueId, url: $url, title: $title, id: $id) { success attachment { ${ATTACHMENT_FIELDS} } }
981
+ }`, { issueId, url, title: title ?? null, id: id ?? null });
982
+ return data.attachmentLinkURL?.attachment;
983
+ },
984
+ });
985
+ rl.registerAction("attachment.delete", {
986
+ description: "Delete an attachment.",
987
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the attachment to delete" } },
988
+ async execute(input, ctx) {
989
+ const data = await gql(key(ctx), `mutation($id: String!) { attachmentDelete(id: $id) { success } }`, { id: input.id });
990
+ return data.attachmentDelete;
991
+ },
992
+ });
993
+ // =========================================================
994
+ // Organization
995
+ // =========================================================
996
+ rl.registerAction("org.get", {
997
+ description: "Get the authenticated workspace.",
998
+ inputSchema: {},
999
+ async execute(_input, ctx) {
1000
+ const data = await gql(key(ctx), `query { organization { ${ORG_FIELDS} } }`);
1001
+ return data.organization;
1002
+ },
1003
+ });
1004
+ // =========================================================
1005
+ // Webhooks
1006
+ // =========================================================
1007
+ listAction("webhook.list", "List webhooks for the current workspace.", "webhooks", null, WEBHOOK_FIELDS);
1008
+ getAction("webhook.get", "Get a webhook by ID.", "webhook", WEBHOOK_FIELDS);
1009
+ rl.registerAction("webhook.create", {
1010
+ description: "Create a webhook. resourceTypes example: ['Issue','Comment','Project'].",
1011
+ inputSchema: {
1012
+ url: { type: "string", required: true, description: "The URL that will be called on data changes" },
1013
+ resourceTypes: { type: "array", required: true, description: "List of resources the webhook should subscribe to (e.g. ['Issue','Comment'])" },
1014
+ label: { type: "string", required: false, description: "Label for the webhook" },
1015
+ teamId: { type: "string", required: false, description: "The identifier or key of the team associated with the webhook. Omit and set allPublicTeams=true for workspace-wide" },
1016
+ allPublicTeams: { type: "boolean", required: false, description: "Whether this webhook is enabled for all public teams" },
1017
+ enabled: { type: "boolean", required: false, description: "Whether this webhook is enabled (default true)" },
1018
+ secret: { type: "string", required: false, description: "A secret token used to sign the webhook payload" },
1019
+ id: { type: "string", required: false, description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" },
1020
+ },
1021
+ async execute(input, ctx) {
1022
+ const data = await gql(key(ctx), `mutation($input: WebhookCreateInput!) { webhookCreate(input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { input: input });
1023
+ return data.webhookCreate?.webhook;
1024
+ },
1025
+ });
1026
+ rl.registerAction("webhook.update", {
1027
+ description: "Update a webhook. teamId and allPublicTeams cannot be changed after creation.",
1028
+ inputSchema: {
1029
+ id: { type: "string", required: true, description: "The identifier of the webhook to update" },
1030
+ url: { type: "string", required: false, description: "The URL that will be called on data changes" },
1031
+ resourceTypes: { type: "array", required: false, description: "List of resources the webhook should subscribe to" },
1032
+ label: { type: "string", required: false, description: "Label for the webhook" },
1033
+ enabled: { type: "boolean", required: false, description: "Whether this webhook is enabled" },
1034
+ secret: { type: "string", required: false, description: "A secret token used to sign the webhook payload" },
1035
+ },
1036
+ async execute(input, ctx) {
1037
+ const { id, ...fields } = input;
1038
+ const data = await gql(key(ctx), `mutation($id: String!, $input: WebhookUpdateInput!) { webhookUpdate(id: $id, input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { id, input: fields });
1039
+ return data.webhookUpdate?.webhook;
1040
+ },
1041
+ });
1042
+ rl.registerAction("webhook.delete", {
1043
+ description: "Delete a webhook.",
1044
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the webhook to delete" } },
1045
+ async execute(input, ctx) {
1046
+ const data = await gql(key(ctx), `mutation($id: String!) { webhookDelete(id: $id) { success } }`, { id: input.id });
1047
+ return data.webhookDelete;
1048
+ },
1049
+ });
1050
+ rl.registerAction("webhook.rotateSecret", {
1051
+ description: "Rotate a webhook's signing secret. Returns the new secret.",
1052
+ inputSchema: { id: { type: "string", required: true, description: "The identifier of the webhook to rotate the secret for" } },
1053
+ async execute(input, ctx) {
1054
+ const data = await gql(key(ctx), `mutation($id: String!) { webhookRotateSecret(id: $id) { success secret } }`, { id: input.id });
1055
+ return data.webhookRotateSecret;
1056
+ },
1057
+ });
200
1058
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runline",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "description": "Code mode for agents \u2014 turn any API or command into a callable action",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",