runline 0.8.1 → 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.
@@ -19,6 +19,84 @@ export async function gql(apiKey, query, variables) {
19
19
  export function key(ctx) {
20
20
  return ctx.connection.config.apiKey;
21
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
+ }
22
100
  export const ISSUE_FIELDS = `id identifier title description url priority estimate dueDate
23
101
  state { id name type } assignee { id name email } creator { id name }
24
102
  team { id key name } project { id name } cycle { id number name }
@@ -106,11 +184,35 @@ export function bindListAction(rl) {
106
184
  export function bindGetAction(rl) {
107
185
  return (...args) => registerGetAction(rl, ...args);
108
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
+ }
109
210
  export function registerListAction(rl, name, description, rootField, filterTypeName, selection) {
110
211
  rl.registerAction(name, {
111
212
  description,
112
213
  inputSchema: t.Object(LIST_INPUT_SCHEMA),
113
214
  async execute(input, ctx) {
215
+ requireRootFieldAvailable(ctx, name, rootField);
114
216
  const opts = (input ?? {});
115
217
  const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
116
218
  const data = await gql(key(ctx), `query${argsDecl} { ${rootField}${argsCall} { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }`, vars);
@@ -124,6 +226,7 @@ export function registerGetAction(rl, name, description, rootField, selection) {
124
226
  description,
125
227
  inputSchema: t.Object({ id: t.String({ description: "Identifier or slug" }) }),
126
228
  async execute(input, ctx) {
229
+ requireRootFieldAvailable(ctx, name, rootField);
127
230
  const data = await gql(key(ctx), `query($id: String!) { ${rootField}(id: $id) { ${selection} } }`, { id: input.id });
128
231
  return data[rootField];
129
232
  },
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { STATE_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
2
+ import { STATE_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
3
  export function registerStateActions(rl) {
4
4
  const listAction = bindListAction(rl);
5
5
  const getAction = bindGetAction(rl);
@@ -17,6 +17,7 @@ export function registerStateActions(rl) {
17
17
  id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
18
18
  }),
19
19
  async execute(input, ctx) {
20
+ requireUnscoped(ctx, "state.create");
20
21
  const data = await gql(key(ctx), `mutation($input: WorkflowStateCreateInput!) { workflowStateCreate(input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { input: input });
21
22
  return data.workflowStateCreate?.workflowState;
22
23
  },
@@ -31,6 +32,7 @@ export function registerStateActions(rl) {
31
32
  position: t.Optional(t.Number({ description: "The position of the state (Float)" })),
32
33
  }),
33
34
  async execute(input, ctx) {
35
+ requireUnscoped(ctx, "state.update");
34
36
  const { id, ...fields } = input;
35
37
  const data = await gql(key(ctx), `mutation($id: String!, $input: WorkflowStateUpdateInput!) { workflowStateUpdate(id: $id, input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { id, input: fields });
36
38
  return data.workflowStateUpdate?.workflowState;
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { TEAM_FIELDS, USER_FIELDS, bindGetAction, bindListAction, gql, key, } from "./shared.js";
2
+ import { TEAM_FIELDS, USER_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped, } from "./shared.js";
3
3
  export function registerTeamActions(rl) {
4
4
  const listAction = bindListAction(rl);
5
5
  const getAction = bindGetAction(rl);
@@ -27,6 +27,7 @@ export function registerTeamActions(rl) {
27
27
  copySettingsFromTeamId: t.Optional(t.String({ description: "The team id to copy settings from, if any" })),
28
28
  }),
29
29
  async execute(input, ctx) {
30
+ requireUnscoped(ctx, "team.create");
30
31
  const { copySettingsFromTeamId, ...fields } = input;
31
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 });
32
33
  return data.teamCreate?.team;
@@ -52,6 +53,7 @@ export function registerTeamActions(rl) {
52
53
  triageEnabled: t.Optional(t.Boolean({ description: "Whether triage mode is enabled for the team" })),
53
54
  }),
54
55
  async execute(input, ctx) {
56
+ requireUnscoped(ctx, "team.update");
55
57
  const { id, ...fields } = input;
56
58
  const data = await gql(key(ctx), `mutation($id: String!, $input: TeamUpdateInput!) { teamUpdate(id: $id, input: $input) { success team { ${TEAM_FIELDS} } } }`, { id, input: fields });
57
59
  return data.teamUpdate?.team;
@@ -64,6 +66,7 @@ export function registerTeamActions(rl) {
64
66
  limit: t.Optional(t.Number({ description: "Max members to return (default 50)" })),
65
67
  }),
66
68
  async execute(input, ctx) {
69
+ requireUnscoped(ctx, "team.members");
67
70
  const { teamId, limit } = input;
68
71
  const data = await gql(key(ctx), `query($id: String!, $first: Int) {
69
72
  team(id: $id) { members(first: $first) { nodes { ${USER_FIELDS} } } }
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { USER_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
2
+ import { USER_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
3
  export function registerUserActions(rl) {
4
4
  const listAction = bindListAction(rl);
5
5
  const getAction = bindGetAction(rl);
@@ -28,6 +28,7 @@ export function registerUserActions(rl) {
28
28
  statusUntilAt: t.Optional(t.String({ description: "When the user status should be cleared (DateTime)" })),
29
29
  }),
30
30
  async execute(input, ctx) {
31
+ requireUnscoped(ctx, "user.update");
31
32
  const { id, ...fields } = input;
32
33
  const data = await gql(key(ctx), `mutation($id: String!, $input: UserUpdateInput!) { userUpdate(id: $id, input: $input) { success user { ${USER_FIELDS} } } }`, { id, input: fields });
33
34
  return data.userUpdate?.user;
@@ -1,5 +1,6 @@
1
1
  import * as t from "typebox";
2
- import { CUSTOM_VIEW_FIELDS, FEED_ITEM_FIELDS, INITIATIVE_FIELDS, ISSUE_LITE, LIST_INPUT_SCHEMA, PROJECT_FIELDS, bindGetAction, bindListAction, buildConnArgs, gql, key, } from "./shared.js";
2
+ import { CUSTOM_VIEW_FIELDS, FEED_ITEM_FIELDS, INITIATIVE_FIELDS, ISSUE_LITE, LIST_INPUT_SCHEMA, PROJECT_FIELDS, bindGetAction, bindListAction, buildConnArgs, gql, key, mergeIssueScopeFilter, requireUnscoped, } from "./shared.js";
3
+ const ISSUE_FILTER_DESCRIPTION = "IssueFilter payload. Examples: label { labels: { id: { in: ['label-id'] } } }; project { project: { id: { eq: 'project-id' } } }; assignee { assignee: { id: { eq: 'user-id' } } }; state { state: { id: { eq: 'state-id' } } }; priority { priority: { eq: 1 } }; due window { dueDate: { gte: '2026-06-09', lte: '2026-06-16' } }; combine with and/or arrays.";
3
4
  export function registerViewActions(rl) {
4
5
  const listAction = bindListAction(rl);
5
6
  const getAction = bindGetAction(rl);
@@ -15,7 +16,12 @@ export function registerViewActions(rl) {
15
16
  }),
16
17
  async execute(input, ctx) {
17
18
  const opts = (input ?? {});
18
- const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
19
+ const scopedOpts = connectionField === "issues"
20
+ ? { ...opts, filter: mergeIssueScopeFilter(ctx, opts.filter) }
21
+ : opts;
22
+ if (connectionField !== "issues")
23
+ requireUnscoped(ctx, name);
24
+ const { argsDecl, argsCall, vars } = buildConnArgs(scopedOpts, filterTypeName);
19
25
  const declParts = ["$id: String!", argsDecl.slice(1, -1)];
20
26
  const callParts = [argsCall.slice(1, -1)];
21
27
  const includeSubTeamsSet = includeSubTeamsDescription !== undefined && opts.includeSubTeams !== undefined;
@@ -35,24 +41,25 @@ export function registerViewActions(rl) {
35
41
  listAction("view.list", "List custom views accessible to the user, including personal and shared workspace views. Linear excludes views scoped to a specific project or initiative from this root query.", "customViews", "CustomViewFilter", CUSTOM_VIEW_FIELDS);
36
42
  getAction("view.get", "Get a custom view by ID or slug.", "customView", CUSTOM_VIEW_FIELDS);
37
43
  rl.registerAction("view.create", {
38
- description: "Create a custom view. Set filterData for issue views; projectFilterData, initiativeFilterData, or feedItemFilterData for other view types.",
44
+ description: "Create a custom view. Set filterData for issue views; projectFilterData, initiativeFilterData, or feedItemFilterData for other view types. Read matches back with view.issues/projects/initiatives/updates.",
39
45
  inputSchema: t.Object({
40
46
  name: t.String({ description: "The name of the custom view" }),
41
47
  description: t.Optional(t.String({ description: "The description of the custom view" })),
42
48
  icon: t.Optional(t.String({ description: "The icon of the custom view" })),
43
49
  color: t.Optional(t.String({ description: "The color of the custom view icon (hex)" })),
44
- shared: t.Optional(t.Boolean({ description: "Whether the custom view is shared with everyone in the workspace" })),
45
- filterData: t.Optional(t.Object({}, { description: "IssueFilter for issue views" })),
50
+ shared: t.Optional(t.Boolean({ description: "false creates a personal view; true shares the view with the workspace or scoped container" })),
51
+ filterData: t.Optional(t.Object({}, { description: ISSUE_FILTER_DESCRIPTION })),
46
52
  projectFilterData: t.Optional(t.Object({}, { description: "ProjectFilter for project views" })),
47
53
  initiativeFilterData: t.Optional(t.Object({}, { description: "InitiativeFilter for initiative views" })),
48
54
  feedItemFilterData: t.Optional(t.Object({}, { description: "FeedItemFilter for update/feed item views" })),
49
- teamId: t.Optional(t.String({ description: "The team associated with the custom view" })),
50
- projectId: t.Optional(t.String({ description: "The project associated with the custom view" })),
51
- initiativeId: t.Optional(t.String({ description: "The initiative associated with the custom view" })),
52
- ownerId: t.Optional(t.String({ description: "The owner of the custom view" })),
55
+ teamId: t.Optional(t.String({ description: "Scope the view to a team" })),
56
+ projectId: t.Optional(t.String({ description: "Scope the view to a project-specific view" })),
57
+ initiativeId: t.Optional(t.String({ description: "Scope the view to an initiative-specific view" })),
58
+ ownerId: t.Optional(t.String({ description: "Set the user that owns the view" })),
53
59
  id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
54
60
  }),
55
61
  async execute(input, ctx) {
62
+ requireUnscoped(ctx, "view.create");
56
63
  const data = await gql(key(ctx), `mutation($input: CustomViewCreateInput!) { customViewCreate(input: $input) { success customView { ${CUSTOM_VIEW_FIELDS} } } }`, { input: input });
57
64
  return data.customViewCreate?.customView;
58
65
  },
@@ -65,17 +72,18 @@ export function registerViewActions(rl) {
65
72
  description: t.Optional(t.String({ description: "The description of the custom view" })),
66
73
  icon: t.Optional(t.String({ description: "The icon of the custom view" })),
67
74
  color: t.Optional(t.String({ description: "The color of the custom view icon (hex)" })),
68
- shared: t.Optional(t.Boolean({ description: "Whether the custom view is shared with everyone in the workspace" })),
69
- filterData: t.Optional(t.Object({}, { description: "IssueFilter for issue views" })),
75
+ shared: t.Optional(t.Boolean({ description: "false creates a personal view; true shares the view with the workspace or scoped container" })),
76
+ filterData: t.Optional(t.Object({}, { description: ISSUE_FILTER_DESCRIPTION })),
70
77
  projectFilterData: t.Optional(t.Object({}, { description: "ProjectFilter for project views" })),
71
78
  initiativeFilterData: t.Optional(t.Object({}, { description: "InitiativeFilter for initiative views" })),
72
79
  feedItemFilterData: t.Optional(t.Object({}, { description: "FeedItemFilter for update/feed item views" })),
73
- teamId: t.Optional(t.String({ description: "The team associated with the custom view" })),
74
- projectId: t.Optional(t.String({ description: "The project associated with the custom view" })),
75
- initiativeId: t.Optional(t.String({ description: "The initiative associated with the custom view" })),
76
- ownerId: t.Optional(t.String({ description: "The owner of the custom view" })),
80
+ teamId: t.Optional(t.String({ description: "Scope the view to a team" })),
81
+ projectId: t.Optional(t.String({ description: "Scope the view to a project-specific view" })),
82
+ initiativeId: t.Optional(t.String({ description: "Scope the view to an initiative-specific view" })),
83
+ ownerId: t.Optional(t.String({ description: "Set the user that owns the view" })),
77
84
  }),
78
85
  async execute(input, ctx) {
86
+ requireUnscoped(ctx, "view.update");
79
87
  const { id, ...fields } = input;
80
88
  const data = await gql(key(ctx), `mutation($id: String!, $input: CustomViewUpdateInput!) { customViewUpdate(id: $id, input: $input) { success customView { ${CUSTOM_VIEW_FIELDS} } } }`, { id, input: fields });
81
89
  return data.customViewUpdate?.customView;
@@ -85,6 +93,7 @@ export function registerViewActions(rl) {
85
93
  description: "Delete a custom view.",
86
94
  inputSchema: t.Object({ id: t.String({ description: "The identifier of the custom view to delete" }) }),
87
95
  async execute(input, ctx) {
96
+ requireUnscoped(ctx, "view.delete");
88
97
  const data = await gql(key(ctx), `mutation($id: String!) { customViewDelete(id: $id) { success } }`, { id: input.id });
89
98
  return data.customViewDelete;
90
99
  },
@@ -1,5 +1,5 @@
1
1
  import * as t from "typebox";
2
- import { WEBHOOK_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
2
+ import { WEBHOOK_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
3
3
  export function registerWebhookActions(rl) {
4
4
  const listAction = bindListAction(rl);
5
5
  const getAction = bindGetAction(rl);
@@ -18,6 +18,7 @@ export function registerWebhookActions(rl) {
18
18
  id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
19
19
  }),
20
20
  async execute(input, ctx) {
21
+ requireUnscoped(ctx, "webhooks.*");
21
22
  const data = await gql(key(ctx), `mutation($input: WebhookCreateInput!) { webhookCreate(input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { input: input });
22
23
  return data.webhookCreate?.webhook;
23
24
  },
@@ -33,6 +34,7 @@ export function registerWebhookActions(rl) {
33
34
  secret: t.Optional(t.String({ description: "A secret token used to sign the webhook payload" })),
34
35
  }),
35
36
  async execute(input, ctx) {
37
+ requireUnscoped(ctx, "webhooks.*");
36
38
  const { id, ...fields } = input;
37
39
  const data = await gql(key(ctx), `mutation($id: String!, $input: WebhookUpdateInput!) { webhookUpdate(id: $id, input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { id, input: fields });
38
40
  return data.webhookUpdate?.webhook;
@@ -42,6 +44,7 @@ export function registerWebhookActions(rl) {
42
44
  description: "Delete a webhook.",
43
45
  inputSchema: t.Object({ id: t.String({ description: "The identifier of the webhook to delete" }) }),
44
46
  async execute(input, ctx) {
47
+ requireUnscoped(ctx, "webhooks.*");
45
48
  const data = await gql(key(ctx), `mutation($id: String!) { webhookDelete(id: $id) { success } }`, { id: input.id });
46
49
  return data.webhookDelete;
47
50
  },
@@ -50,6 +53,7 @@ export function registerWebhookActions(rl) {
50
53
  description: "Rotate a webhook's signing secret. Returns the new secret.",
51
54
  inputSchema: t.Object({ id: t.String({ description: "The identifier of the webhook to rotate the secret for" }) }),
52
55
  async execute(input, ctx) {
56
+ requireUnscoped(ctx, "webhooks.*");
53
57
  const data = await gql(key(ctx), `mutation($id: String!) { webhookRotateSecret(id: $id) { success secret } }`, { id: input.id });
54
58
  return data.webhookRotateSecret;
55
59
  },
@@ -0,0 +1,11 @@
1
+ import * as t from "typebox";
2
+ import { TEAM_INPUT_SCHEMA, api } from "./shared.js";
3
+ export function registerAccountActions(rl) {
4
+ rl.registerAction("whoami", {
5
+ description: "Validate the Vercel token and return the authenticated user/account context.",
6
+ inputSchema: t.Object(TEAM_INPUT_SCHEMA),
7
+ async execute(input, ctx) {
8
+ return api(ctx, "/v2/user", { query: (input ?? {}) });
9
+ },
10
+ });
11
+ }
@@ -0,0 +1,79 @@
1
+ import * as t from "typebox";
2
+ import { LIST_INPUT_SCHEMA, TEAM_INPUT_SCHEMA, api, bindGetAction } from "./shared.js";
3
+ export function registerDeploymentActions(rl) {
4
+ const getAction = bindGetAction(rl);
5
+ rl.registerAction("deployment.list", {
6
+ description: "List Vercel deployments. Filter by project, state, target, user, or time window.",
7
+ inputSchema: t.Object({
8
+ ...LIST_INPUT_SCHEMA,
9
+ projectId: t.Optional(t.String({ description: "Project ID to filter deployments" })),
10
+ projectIds: t.Optional(t.Array(t.String(), { description: "Project IDs to filter deployments" })),
11
+ app: t.Optional(t.String({ description: "Project name/app filter" })),
12
+ target: t.Optional(t.String({ description: "production, preview, or a custom target" })),
13
+ state: t.Optional(t.String({ description: "Comma-separated deployment states, e.g. BUILDING,READY,ERROR" })),
14
+ users: t.Optional(t.String({ description: "Comma-separated Vercel user IDs" })),
15
+ }),
16
+ async execute(input, ctx) {
17
+ return api(ctx, "/v7/deployments", { query: (input ?? {}) });
18
+ },
19
+ });
20
+ getAction("deployment.get", "Get a Vercel deployment by ID or URL.", (id) => `/v13/deployments/${encodeURIComponent(id)}`);
21
+ rl.registerAction("deployment.logs", {
22
+ description: "Get build/deployment logs/events for a deployment. Use builds=1 for build logs and limit/since/until to avoid huge responses.",
23
+ inputSchema: t.Object({
24
+ ...TEAM_INPUT_SCHEMA,
25
+ idOrUrl: t.String({ description: "Deployment ID or URL" }),
26
+ limit: t.Optional(t.Number({ description: "Maximum events. Vercel supports -1 for all available logs" })),
27
+ direction: t.Optional(t.String({ description: "forward or backward" })),
28
+ follow: t.Optional(t.Number({ description: "0 or 1. Avoid 1 in short-lived agent calls unless intentionally streaming" })),
29
+ name: t.Optional(t.String({ description: "Build ID/name" })),
30
+ since: t.Optional(t.Number({ description: "Start timestamp in milliseconds" })),
31
+ until: t.Optional(t.Number({ description: "End timestamp in milliseconds" })),
32
+ statusCode: t.Optional(t.String({ description: "HTTP status filter such as 5xx" })),
33
+ delimiter: t.Optional(t.Number({ description: "0 or 1" })),
34
+ builds: t.Optional(t.Number({ description: "0 or 1" })),
35
+ }),
36
+ async execute(input, ctx) {
37
+ const { idOrUrl, ...query } = input;
38
+ return api(ctx, `/v3/deployments/${encodeURIComponent(String(idOrUrl))}/events`, { query });
39
+ },
40
+ });
41
+ rl.registerAction("deployment.runtimeLogs", {
42
+ description: "Get runtime logs for a deployment. Requires projectId and deploymentId; use limit/since/until to keep responses bounded.",
43
+ inputSchema: t.Object({
44
+ ...TEAM_INPUT_SCHEMA,
45
+ projectId: t.String({ description: "Project ID" }),
46
+ deploymentId: t.String({ description: "Deployment ID" }),
47
+ limit: t.Optional(t.Number({ description: "Maximum logs when supported by Vercel" })),
48
+ since: t.Optional(t.Number({ description: "Start timestamp in milliseconds" })),
49
+ until: t.Optional(t.Number({ description: "End timestamp in milliseconds" })),
50
+ }),
51
+ async execute(input, ctx) {
52
+ const { projectId, deploymentId, ...query } = input;
53
+ return api(ctx, `/v1/projects/${encodeURIComponent(String(projectId))}/deployments/${encodeURIComponent(String(deploymentId))}/runtime-logs`, { query });
54
+ },
55
+ });
56
+ rl.registerAction("deployment.cancel", {
57
+ description: "Cancel a queued or building Vercel deployment.",
58
+ inputSchema: t.Object({
59
+ ...TEAM_INPUT_SCHEMA,
60
+ id: t.String({ description: "Deployment ID" }),
61
+ }),
62
+ async execute(input, ctx) {
63
+ const { id, ...query } = input;
64
+ return api(ctx, `/v12/deployments/${encodeURIComponent(id)}/cancel`, { method: "PATCH", query });
65
+ },
66
+ });
67
+ rl.registerAction("deployment.promote", {
68
+ description: "Promote an existing deployment to production for a project, where supported by Vercel.",
69
+ inputSchema: t.Object({
70
+ ...TEAM_INPUT_SCHEMA,
71
+ projectId: t.String({ description: "Project ID" }),
72
+ deploymentId: t.String({ description: "Deployment ID to promote" }),
73
+ }),
74
+ async execute(input, ctx) {
75
+ const { projectId, deploymentId, ...query } = input;
76
+ return api(ctx, `/v10/projects/${encodeURIComponent(String(projectId))}/promote/${encodeURIComponent(String(deploymentId))}`, { method: "POST", query });
77
+ },
78
+ });
79
+ }
@@ -0,0 +1,101 @@
1
+ import * as t from "typebox";
2
+ import { TEAM_INPUT_SCHEMA, api } from "./shared.js";
3
+ const targetSchema = t.Union([t.String(), t.Array(t.String())], {
4
+ description: "production, preview, development, custom environment, or array of targets",
5
+ });
6
+ function normalizeEnvBody(body) {
7
+ const normalized = { ...body };
8
+ if (typeof normalized.target === "string")
9
+ normalized.target = [normalized.target];
10
+ return normalized;
11
+ }
12
+ function assertCreateEnvInput(body) {
13
+ if (!body.key || !body.value || !body.type) {
14
+ throw new Error("env.set create requires key, value, and type");
15
+ }
16
+ if (!body.target && !body.customEnvironmentIds) {
17
+ throw new Error("env.set create requires target or customEnvironmentIds");
18
+ }
19
+ }
20
+ export function registerEnvActions(rl) {
21
+ rl.registerAction("env.list", {
22
+ description: "List environment variables for a Vercel project.",
23
+ inputSchema: t.Object({
24
+ ...TEAM_INPUT_SCHEMA,
25
+ projectIdOrName: t.String({ description: "Project ID or name" }),
26
+ target: t.Optional(t.String({ description: "production, preview, development, or custom environment" })),
27
+ gitBranch: t.Optional(t.String({ description: "Git branch filter" })),
28
+ decrypt: t.Optional(t.Boolean({ description: "Ask Vercel to include decrypted values when permitted" })),
29
+ source: t.Optional(t.String({ description: "Environment variable source filter" })),
30
+ }),
31
+ async execute(input, ctx) {
32
+ const { projectIdOrName, ...query } = input;
33
+ return api(ctx, `/v10/projects/${encodeURIComponent(String(projectIdOrName))}/env`, { query });
34
+ },
35
+ });
36
+ rl.registerAction("env.get", {
37
+ description: "Get one environment variable by ID for a Vercel project, including decrypted value when permitted.",
38
+ inputSchema: t.Object({
39
+ ...TEAM_INPUT_SCHEMA,
40
+ projectIdOrName: t.String({ description: "Project ID or name" }),
41
+ id: t.String({ description: "Environment variable ID" }),
42
+ }),
43
+ async execute(input, ctx) {
44
+ const { projectIdOrName, id, ...query } = input;
45
+ return api(ctx, `/v1/projects/${encodeURIComponent(String(projectIdOrName))}/env/${encodeURIComponent(String(id))}`, { query });
46
+ },
47
+ });
48
+ rl.registerAction("env.set", {
49
+ description: "Create or update a Vercel project environment variable. Without id, creates a variable and requires key, value, type, and target/customEnvironmentIds. With id, updates that exact variable. Be explicit about target to avoid changing the wrong environment.",
50
+ inputSchema: t.Object({
51
+ ...TEAM_INPUT_SCHEMA,
52
+ projectIdOrName: t.String({ description: "Project ID or name" }),
53
+ id: t.Optional(t.String({ description: "Environment variable ID. When provided, env.set updates instead of creating." })),
54
+ key: t.Optional(t.String({ description: "Variable name" })),
55
+ value: t.Optional(t.String({ description: "Variable value" })),
56
+ target: t.Optional(targetSchema),
57
+ customEnvironmentIds: t.Optional(t.Array(t.String(), { description: "Custom environment IDs for custom-environment scoped variables" })),
58
+ type: t.Optional(t.String({ description: "Vercel env var type: system, encrypted, plain, or sensitive" })),
59
+ gitBranch: t.Optional(t.String({ description: "Git branch for branch-scoped preview variables" })),
60
+ comment: t.Optional(t.String({ description: "Optional comment" })),
61
+ variables: t.Optional(t.Array(t.Object({}, { description: "Raw Vercel env var objects for batch create" }))),
62
+ }),
63
+ async execute(input, ctx) {
64
+ const { projectIdOrName, id, teamId, slug, variables, ...fields } = input;
65
+ const query = { teamId, slug };
66
+ if (id) {
67
+ return api(ctx, `/v9/projects/${encodeURIComponent(String(projectIdOrName))}/env/${encodeURIComponent(String(id))}`, {
68
+ method: "PATCH",
69
+ query,
70
+ body: normalizeEnvBody(fields),
71
+ });
72
+ }
73
+ if (Array.isArray(variables)) {
74
+ return api(ctx, `/v10/projects/${encodeURIComponent(String(projectIdOrName))}/env`, {
75
+ method: "POST",
76
+ query,
77
+ body: variables.map((item) => normalizeEnvBody(item)),
78
+ });
79
+ }
80
+ const body = normalizeEnvBody(fields);
81
+ assertCreateEnvInput(body);
82
+ return api(ctx, `/v10/projects/${encodeURIComponent(String(projectIdOrName))}/env`, { method: "POST", query, body });
83
+ },
84
+ });
85
+ rl.registerAction("env.delete", {
86
+ description: "Delete an environment variable from a Vercel project. This removes the variable for the specified project/environment scope.",
87
+ inputSchema: t.Object({
88
+ ...TEAM_INPUT_SCHEMA,
89
+ projectIdOrName: t.String({ description: "Project ID or name" }),
90
+ id: t.String({ description: "Environment variable ID" }),
91
+ customEnvironmentId: t.Optional(t.String({ description: "Custom environment ID when required" })),
92
+ }),
93
+ async execute(input, ctx) {
94
+ const { projectIdOrName, id, customEnvironmentId, ...query } = input;
95
+ return api(ctx, `/v9/projects/${encodeURIComponent(String(projectIdOrName))}/env/${encodeURIComponent(String(id))}`, {
96
+ method: "DELETE",
97
+ query: { ...query, customEnvironmentId },
98
+ });
99
+ },
100
+ });
101
+ }
@@ -0,0 +1,27 @@
1
+ import * as t from "typebox";
2
+ import { registerAccountActions } from "./account.js";
3
+ import { registerDeploymentActions } from "./deployments.js";
4
+ import { registerEnvActions } from "./env.js";
5
+ import { registerProjectActions } from "./projects.js";
6
+ export default function vercel(rl) {
7
+ rl.setName("vercel");
8
+ rl.setVersion("0.1.0");
9
+ rl.setConnectionSchema(t.Object({
10
+ token: t.String({
11
+ description: "Vercel access token (https://vercel.com/account/settings/tokens)",
12
+ env: "VERCEL_TOKEN",
13
+ }),
14
+ teamId: t.Optional(t.String({
15
+ description: "Optional Vercel Team ID. Added as teamId to every API request.",
16
+ env: "VERCEL_TEAM_ID",
17
+ })),
18
+ slug: t.Optional(t.String({
19
+ description: "Optional Vercel Team slug. Added as slug to every API request when teamId is not used.",
20
+ env: "VERCEL_TEAM_SLUG",
21
+ })),
22
+ }));
23
+ registerAccountActions(rl);
24
+ registerProjectActions(rl);
25
+ registerDeploymentActions(rl);
26
+ registerEnvActions(rl);
27
+ }
@@ -0,0 +1,29 @@
1
+ import * as t from "typebox";
2
+ import { LIST_INPUT_SCHEMA, TEAM_INPUT_SCHEMA, api, bindGetAction } from "./shared.js";
3
+ export function registerProjectActions(rl) {
4
+ const getAction = bindGetAction(rl);
5
+ rl.registerAction("project.list", {
6
+ description: "List Vercel projects visible to the token.",
7
+ inputSchema: t.Object({
8
+ ...LIST_INPUT_SCHEMA,
9
+ search: t.Optional(t.String({ description: "Search by project name" })),
10
+ }),
11
+ async execute(input, ctx) {
12
+ const opts = (input ?? {});
13
+ return api(ctx, "/v10/projects", { query: opts });
14
+ },
15
+ });
16
+ getAction("project.get", "Get a Vercel project by ID or name.", (id) => `/v9/projects/${encodeURIComponent(id)}`);
17
+ rl.registerAction("project.domains", {
18
+ description: "List domains configured for a Vercel project.",
19
+ inputSchema: t.Object({
20
+ ...TEAM_INPUT_SCHEMA,
21
+ projectIdOrName: t.String({ description: "Project ID or name" }),
22
+ limit: t.Optional(t.Number({ description: "Maximum number of domains" })),
23
+ }),
24
+ async execute(input, ctx) {
25
+ const { projectIdOrName, ...query } = input;
26
+ return api(ctx, `/v9/projects/${encodeURIComponent(String(projectIdOrName))}/domains`, { query });
27
+ },
28
+ });
29
+ }