runline 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth.js +4 -2
- package/dist/config/loader.d.ts +2 -4
- package/dist/config/loader.js +2 -1
- package/dist/core/engine.js +18 -13
- package/dist/index.d.ts +2 -2
- package/dist/plugin/api.d.ts +2 -9
- package/dist/plugin/api.js +1 -4
- package/dist/plugin/loader.js +41 -25
- package/dist/plugin/schema.d.ts +19 -0
- package/dist/plugin/schema.js +168 -0
- package/dist/plugin/types.d.ts +22 -8
- package/dist/plugins/gmail/src/index.js +14 -2
- package/dist/plugins/linear/src/attachments.js +87 -0
- package/dist/plugins/linear/src/comments.js +64 -0
- package/dist/plugins/linear/src/cycles.js +42 -0
- package/dist/plugins/linear/src/index.js +35 -1153
- package/dist/plugins/linear/src/initiatives.js +84 -0
- package/dist/plugins/linear/src/issues.js +267 -0
- package/dist/plugins/linear/src/labels.js +74 -0
- package/dist/plugins/linear/src/organization.js +13 -0
- package/dist/plugins/linear/src/projects.js +200 -0
- package/dist/plugins/linear/src/shared.js +234 -0
- package/dist/plugins/linear/src/states.js +41 -0
- package/dist/plugins/linear/src/teams.js +77 -0
- package/dist/plugins/linear/src/users.js +37 -0
- package/dist/plugins/linear/src/views.js +105 -0
- package/dist/plugins/linear/src/webhooks.js +61 -0
- package/dist/plugins/vercel/src/account.js +11 -0
- package/dist/plugins/vercel/src/deployments.js +79 -0
- package/dist/plugins/vercel/src/env.js +101 -0
- package/dist/plugins/vercel/src/index.js +27 -0
- package/dist/plugins/vercel/src/projects.js +29 -0
- package/dist/plugins/vercel/src/shared.js +73 -0
- package/package.json +3 -2
|
@@ -0,0 +1,105 @@
|
|
|
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, 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.";
|
|
4
|
+
export function registerViewActions(rl) {
|
|
5
|
+
const listAction = bindListAction(rl);
|
|
6
|
+
const getAction = bindGetAction(rl);
|
|
7
|
+
function customViewConnectionAction(name, description, connectionField, filterTypeName, selection, includeSubTeamsDescription) {
|
|
8
|
+
rl.registerAction(name, {
|
|
9
|
+
description,
|
|
10
|
+
inputSchema: t.Object({
|
|
11
|
+
viewId: t.String({ description: "The custom view ID or slug" }),
|
|
12
|
+
...LIST_INPUT_SCHEMA,
|
|
13
|
+
...(includeSubTeamsDescription
|
|
14
|
+
? { includeSubTeams: t.Optional(t.Boolean({ description: includeSubTeamsDescription })) }
|
|
15
|
+
: {}),
|
|
16
|
+
}),
|
|
17
|
+
async execute(input, ctx) {
|
|
18
|
+
const opts = (input ?? {});
|
|
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);
|
|
25
|
+
const declParts = ["$id: String!", argsDecl.slice(1, -1)];
|
|
26
|
+
const callParts = [argsCall.slice(1, -1)];
|
|
27
|
+
const includeSubTeamsSet = includeSubTeamsDescription !== undefined && opts.includeSubTeams !== undefined;
|
|
28
|
+
if (includeSubTeamsSet) {
|
|
29
|
+
declParts.push("$includeSubTeams: Boolean");
|
|
30
|
+
callParts.push("includeSubTeams: $includeSubTeams");
|
|
31
|
+
vars.includeSubTeams = opts.includeSubTeams;
|
|
32
|
+
}
|
|
33
|
+
const data = await gql(key(ctx), `query(${declParts.join(", ")}) {
|
|
34
|
+
customView(id: $id) { ${connectionField}(${callParts.join(", ")}) { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }
|
|
35
|
+
}`, { id: opts.viewId, ...vars });
|
|
36
|
+
const conn = (data.customView?.[connectionField] ?? {});
|
|
37
|
+
return { nodes: conn.nodes, pageInfo: conn.pageInfo };
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
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);
|
|
42
|
+
getAction("view.get", "Get a custom view by ID or slug.", "customView", CUSTOM_VIEW_FIELDS);
|
|
43
|
+
rl.registerAction("view.create", {
|
|
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.",
|
|
45
|
+
inputSchema: t.Object({
|
|
46
|
+
name: t.String({ description: "The name of the custom view" }),
|
|
47
|
+
description: t.Optional(t.String({ description: "The description of the custom view" })),
|
|
48
|
+
icon: t.Optional(t.String({ description: "The icon of the custom view" })),
|
|
49
|
+
color: t.Optional(t.String({ description: "The color of the custom view icon (hex)" })),
|
|
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 })),
|
|
52
|
+
projectFilterData: t.Optional(t.Object({}, { description: "ProjectFilter for project views" })),
|
|
53
|
+
initiativeFilterData: t.Optional(t.Object({}, { description: "InitiativeFilter for initiative views" })),
|
|
54
|
+
feedItemFilterData: t.Optional(t.Object({}, { description: "FeedItemFilter for update/feed item views" })),
|
|
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" })),
|
|
59
|
+
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
60
|
+
}),
|
|
61
|
+
async execute(input, ctx) {
|
|
62
|
+
requireUnscoped(ctx, "view.create");
|
|
63
|
+
const data = await gql(key(ctx), `mutation($input: CustomViewCreateInput!) { customViewCreate(input: $input) { success customView { ${CUSTOM_VIEW_FIELDS} } } }`, { input: input });
|
|
64
|
+
return data.customViewCreate?.customView;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
rl.registerAction("view.update", {
|
|
68
|
+
description: "Update a custom view. All fields optional; only provided fields are updated.",
|
|
69
|
+
inputSchema: t.Object({
|
|
70
|
+
id: t.String({ description: "The identifier of the custom view to update" }),
|
|
71
|
+
name: t.Optional(t.String({ description: "The name of the custom view" })),
|
|
72
|
+
description: t.Optional(t.String({ description: "The description of the custom view" })),
|
|
73
|
+
icon: t.Optional(t.String({ description: "The icon of the custom view" })),
|
|
74
|
+
color: t.Optional(t.String({ description: "The color of the custom view icon (hex)" })),
|
|
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 })),
|
|
77
|
+
projectFilterData: t.Optional(t.Object({}, { description: "ProjectFilter for project views" })),
|
|
78
|
+
initiativeFilterData: t.Optional(t.Object({}, { description: "InitiativeFilter for initiative views" })),
|
|
79
|
+
feedItemFilterData: t.Optional(t.Object({}, { description: "FeedItemFilter for update/feed item views" })),
|
|
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" })),
|
|
84
|
+
}),
|
|
85
|
+
async execute(input, ctx) {
|
|
86
|
+
requireUnscoped(ctx, "view.update");
|
|
87
|
+
const { id, ...fields } = input;
|
|
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 });
|
|
89
|
+
return data.customViewUpdate?.customView;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
rl.registerAction("view.delete", {
|
|
93
|
+
description: "Delete a custom view.",
|
|
94
|
+
inputSchema: t.Object({ id: t.String({ description: "The identifier of the custom view to delete" }) }),
|
|
95
|
+
async execute(input, ctx) {
|
|
96
|
+
requireUnscoped(ctx, "view.delete");
|
|
97
|
+
const data = await gql(key(ctx), `mutation($id: String!) { customViewDelete(id: $id) { success } }`, { id: input.id });
|
|
98
|
+
return data.customViewDelete;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
customViewConnectionAction("view.issues", "List issues matching a custom view's issue filter. Returns an empty connection when the view's modelName is not Issue.", "issues", "IssueFilter", ISSUE_LITE, "Include issues from sub-teams when the custom view is associated with a team");
|
|
102
|
+
customViewConnectionAction("view.projects", "List projects matching a custom view's project filter. Returns an empty connection when the view's modelName is not Project.", "projects", "ProjectFilter", PROJECT_FIELDS, "Include projects from sub-teams when the custom view is associated with a team");
|
|
103
|
+
customViewConnectionAction("view.initiatives", "List initiatives matching a custom view's initiative filter. Returns an empty connection when the view's modelName is not Initiative.", "initiatives", "InitiativeFilter", INITIATIVE_FIELDS);
|
|
104
|
+
customViewConnectionAction("view.updates", "List feed items matching a custom view's feed item filter. Returns an empty connection when the view's modelName is not FeedItem.", "updates", "FeedItemFilter", FEED_ITEM_FIELDS, "Include updates from sub-teams when the custom view is associated with a team");
|
|
105
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { WEBHOOK_FIELDS, bindGetAction, bindListAction, gql, key, requireUnscoped } from "./shared.js";
|
|
3
|
+
export function registerWebhookActions(rl) {
|
|
4
|
+
const listAction = bindListAction(rl);
|
|
5
|
+
const getAction = bindGetAction(rl);
|
|
6
|
+
listAction("webhook.list", "List webhooks for the current workspace.", "webhooks", null, WEBHOOK_FIELDS);
|
|
7
|
+
getAction("webhook.get", "Get a webhook by ID.", "webhook", WEBHOOK_FIELDS);
|
|
8
|
+
rl.registerAction("webhook.create", {
|
|
9
|
+
description: "Create a webhook. resourceTypes example: ['Issue','Comment','Project'].",
|
|
10
|
+
inputSchema: t.Object({
|
|
11
|
+
url: t.String({ description: "The URL that will be called on data changes" }),
|
|
12
|
+
resourceTypes: t.Array(t.String(), { description: "List of resources the webhook should subscribe to (e.g. ['Issue','Comment'])" }),
|
|
13
|
+
label: t.Optional(t.String({ description: "Label for the webhook" })),
|
|
14
|
+
teamId: t.Optional(t.String({ description: "The identifier or key of the team associated with the webhook. Omit and set allPublicTeams=true for workspace-wide" })),
|
|
15
|
+
allPublicTeams: t.Optional(t.Boolean({ description: "Whether this webhook is enabled for all public teams" })),
|
|
16
|
+
enabled: t.Optional(t.Boolean({ description: "Whether this webhook is enabled (default true)" })),
|
|
17
|
+
secret: t.Optional(t.String({ description: "A secret token used to sign the webhook payload" })),
|
|
18
|
+
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
19
|
+
}),
|
|
20
|
+
async execute(input, ctx) {
|
|
21
|
+
requireUnscoped(ctx, "webhooks.*");
|
|
22
|
+
const data = await gql(key(ctx), `mutation($input: WebhookCreateInput!) { webhookCreate(input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { input: input });
|
|
23
|
+
return data.webhookCreate?.webhook;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
rl.registerAction("webhook.update", {
|
|
27
|
+
description: "Update a webhook. teamId and allPublicTeams cannot be changed after creation.",
|
|
28
|
+
inputSchema: t.Object({
|
|
29
|
+
id: t.String({ description: "The identifier of the webhook to update" }),
|
|
30
|
+
url: t.Optional(t.String({ description: "The URL that will be called on data changes" })),
|
|
31
|
+
resourceTypes: t.Optional(t.Array(t.String(), { description: "List of resources the webhook should subscribe to" })),
|
|
32
|
+
label: t.Optional(t.String({ description: "Label for the webhook" })),
|
|
33
|
+
enabled: t.Optional(t.Boolean({ description: "Whether this webhook is enabled" })),
|
|
34
|
+
secret: t.Optional(t.String({ description: "A secret token used to sign the webhook payload" })),
|
|
35
|
+
}),
|
|
36
|
+
async execute(input, ctx) {
|
|
37
|
+
requireUnscoped(ctx, "webhooks.*");
|
|
38
|
+
const { id, ...fields } = input;
|
|
39
|
+
const data = await gql(key(ctx), `mutation($id: String!, $input: WebhookUpdateInput!) { webhookUpdate(id: $id, input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { id, input: fields });
|
|
40
|
+
return data.webhookUpdate?.webhook;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
rl.registerAction("webhook.delete", {
|
|
44
|
+
description: "Delete a webhook.",
|
|
45
|
+
inputSchema: t.Object({ id: t.String({ description: "The identifier of the webhook to delete" }) }),
|
|
46
|
+
async execute(input, ctx) {
|
|
47
|
+
requireUnscoped(ctx, "webhooks.*");
|
|
48
|
+
const data = await gql(key(ctx), `mutation($id: String!) { webhookDelete(id: $id) { success } }`, { id: input.id });
|
|
49
|
+
return data.webhookDelete;
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
rl.registerAction("webhook.rotateSecret", {
|
|
53
|
+
description: "Rotate a webhook's signing secret. Returns the new secret.",
|
|
54
|
+
inputSchema: t.Object({ id: t.String({ description: "The identifier of the webhook to rotate the secret for" }) }),
|
|
55
|
+
async execute(input, ctx) {
|
|
56
|
+
requireUnscoped(ctx, "webhooks.*");
|
|
57
|
+
const data = await gql(key(ctx), `mutation($id: String!) { webhookRotateSecret(id: $id) { success secret } }`, { id: input.id });
|
|
58
|
+
return data.webhookRotateSecret;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
const BASE_URL = "https://api.vercel.com";
|
|
3
|
+
export function token(ctx) {
|
|
4
|
+
return ctx.connection.config.token;
|
|
5
|
+
}
|
|
6
|
+
export async function api(ctx, path, options = {}) {
|
|
7
|
+
const url = new URL(path, BASE_URL);
|
|
8
|
+
const query = {
|
|
9
|
+
teamId: ctx.connection.config.teamId,
|
|
10
|
+
slug: ctx.connection.config.slug,
|
|
11
|
+
...(options.query ?? {}),
|
|
12
|
+
};
|
|
13
|
+
for (const [key, value] of Object.entries(query)) {
|
|
14
|
+
if (value === undefined || value === null || value === "")
|
|
15
|
+
continue;
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
for (const item of value)
|
|
18
|
+
url.searchParams.append(key, String(item));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
url.searchParams.set(key, String(value));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const init = {
|
|
25
|
+
method: options.method ?? "GET",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token(ctx)}`,
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
...(options.headers ?? {}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
if (options.body !== undefined)
|
|
33
|
+
init.body = JSON.stringify(options.body);
|
|
34
|
+
const res = await fetch(url.toString(), init);
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
throw new Error(`Vercel API error ${res.status}: ${text || res.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
if (!text)
|
|
40
|
+
return {};
|
|
41
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
42
|
+
if (contentType.includes("application/json") || text.startsWith("{") || text.startsWith("[")) {
|
|
43
|
+
return JSON.parse(text);
|
|
44
|
+
}
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
export const TEAM_INPUT_SCHEMA = {
|
|
48
|
+
teamId: t.Optional(t.String({ description: "Override the configured Vercel Team ID for this call" })),
|
|
49
|
+
slug: t.Optional(t.String({ description: "Override the configured Vercel Team slug for this call" })),
|
|
50
|
+
};
|
|
51
|
+
export const LIST_INPUT_SCHEMA = {
|
|
52
|
+
...TEAM_INPUT_SCHEMA,
|
|
53
|
+
limit: t.Optional(t.Number({ description: "Maximum number of results" })),
|
|
54
|
+
since: t.Optional(t.Number({ description: "Timestamp in milliseconds to start from" })),
|
|
55
|
+
until: t.Optional(t.Number({ description: "Timestamp in milliseconds to end at" })),
|
|
56
|
+
from: t.Optional(t.Number({ description: "Pagination timestamp/cursor supported by Vercel" })),
|
|
57
|
+
to: t.Optional(t.Number({ description: "Pagination timestamp/cursor supported by Vercel" })),
|
|
58
|
+
};
|
|
59
|
+
export function bindGetAction(rl) {
|
|
60
|
+
return (name, description, pathForId) => {
|
|
61
|
+
rl.registerAction(name, {
|
|
62
|
+
description,
|
|
63
|
+
inputSchema: t.Object({
|
|
64
|
+
id: t.String({ description: "Resource ID, name, or URL" }),
|
|
65
|
+
...TEAM_INPUT_SCHEMA,
|
|
66
|
+
}),
|
|
67
|
+
async execute(input, ctx) {
|
|
68
|
+
const { id, ...query } = input;
|
|
69
|
+
return api(ctx, pathForId(id), { query });
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Code mode for agents — turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"jiti": "^2.7.0",
|
|
69
69
|
"proper-lockfile": "^4.1.2",
|
|
70
70
|
"quickjs-emscripten": "^0.32.0",
|
|
71
|
-
"rrule": "^2.8.1"
|
|
71
|
+
"rrule": "^2.8.1",
|
|
72
|
+
"typebox": "^1.1.35"
|
|
72
73
|
}
|
|
73
74
|
}
|