runline 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +65 -0
- package/dist/plugins/linear/src/comments.js +50 -0
- package/dist/plugins/linear/src/cycles.js +40 -0
- package/dist/plugins/linear/src/index.js +31 -1153
- package/dist/plugins/linear/src/initiatives.js +79 -0
- package/dist/plugins/linear/src/issues.js +245 -0
- package/dist/plugins/linear/src/labels.js +69 -0
- package/dist/plugins/linear/src/organization.js +12 -0
- package/dist/plugins/linear/src/projects.js +189 -0
- package/dist/plugins/linear/src/shared.js +131 -0
- package/dist/plugins/linear/src/states.js +39 -0
- package/dist/plugins/linear/src/teams.js +74 -0
- package/dist/plugins/linear/src/users.js +36 -0
- package/dist/plugins/linear/src/views.js +96 -0
- package/dist/plugins/linear/src/webhooks.js +57 -0
- package/package.json +3 -2
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
const GQL_URL = "https://api.linear.app/graphql";
|
|
3
|
+
export async function gql(apiKey, query, variables) {
|
|
4
|
+
const body = { query };
|
|
5
|
+
if (variables)
|
|
6
|
+
body.variables = variables;
|
|
7
|
+
const res = await fetch(GQL_URL, {
|
|
8
|
+
method: "POST",
|
|
9
|
+
headers: { Authorization: apiKey, "Content-Type": "application/json" },
|
|
10
|
+
body: JSON.stringify(body),
|
|
11
|
+
});
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new Error(`Linear API error ${res.status}: ${await res.text()}`);
|
|
14
|
+
const data = (await res.json());
|
|
15
|
+
if (data.errors)
|
|
16
|
+
throw new Error(`Linear GraphQL error: ${JSON.stringify(data.errors)}`);
|
|
17
|
+
return data.data;
|
|
18
|
+
}
|
|
19
|
+
export function key(ctx) {
|
|
20
|
+
return ctx.connection.config.apiKey;
|
|
21
|
+
}
|
|
22
|
+
export const ISSUE_FIELDS = `id identifier title description url priority estimate dueDate
|
|
23
|
+
state { id name type } assignee { id name email } creator { id name }
|
|
24
|
+
team { id key name } project { id name } cycle { id number name }
|
|
25
|
+
projectMilestone { id name } parent { id identifier }
|
|
26
|
+
labels { nodes { id name color } }
|
|
27
|
+
createdAt updatedAt completedAt canceledAt archivedAt`;
|
|
28
|
+
export const ISSUE_LITE = `id identifier title url priority state { id name type } assignee { id name } team { key } updatedAt`;
|
|
29
|
+
export const COMMENT_FIELDS = `id body url issue { id identifier } user { id name } parent { id } createdAt updatedAt editedAt resolvedAt`;
|
|
30
|
+
export const STATE_FIELDS = `id name type color position description team { id key }`;
|
|
31
|
+
export const LABEL_FIELDS = `id name color description isGroup parent { id name } team { id key } createdAt`;
|
|
32
|
+
export const PROJECT_FIELDS = `id name description url icon color priority progress health
|
|
33
|
+
state status { id name type } lead { id name } startDate targetDate
|
|
34
|
+
teams { nodes { id key } } createdAt updatedAt completedAt canceledAt`;
|
|
35
|
+
export const MILESTONE_FIELDS = `id name description targetDate sortOrder project { id name } createdAt updatedAt`;
|
|
36
|
+
export const PROJECT_UPDATE_FIELDS = `id body health url user { id name } project { id name } createdAt`;
|
|
37
|
+
export const FEED_ITEM_FIELDS = `id createdAt updatedAt archivedAt team { id key name } user { id name }
|
|
38
|
+
projectUpdate { ${PROJECT_UPDATE_FIELDS} }
|
|
39
|
+
initiativeUpdate { id body health url user { id name } initiative { id name } createdAt }
|
|
40
|
+
post { id title body slugId type creator { id name } createdAt updatedAt }`;
|
|
41
|
+
export const CUSTOM_VIEW_FIELDS = `id name description icon color shared slugId modelName
|
|
42
|
+
filterData projectFilterData initiativeFilterData feedItemFilterData
|
|
43
|
+
team { id key name } owner { id name } creator { id name }
|
|
44
|
+
createdAt updatedAt archivedAt`;
|
|
45
|
+
export const CYCLE_FIELDS = `id number name description startsAt endsAt completedAt progress team { id key } createdAt`;
|
|
46
|
+
export const INITIATIVE_FIELDS = `id name description url icon color status targetDate owner { id name }
|
|
47
|
+
projects { nodes { id name } } createdAt updatedAt completedAt`;
|
|
48
|
+
export const TEAM_FIELDS = `id key name description icon color private timezone
|
|
49
|
+
cyclesEnabled cycleDuration issueEstimationType triageEnabled
|
|
50
|
+
parent { id key } createdAt`;
|
|
51
|
+
export const USER_FIELDS = `id name displayName email avatarUrl active admin guest
|
|
52
|
+
isMe statusEmoji statusLabel createdAt`;
|
|
53
|
+
export const ATTACHMENT_FIELDS = `id title subtitle url sourceType groupBySource metadata
|
|
54
|
+
issue { id identifier } creator { id name } createdAt updatedAt`;
|
|
55
|
+
export const ORG_FIELDS = `id name urlKey logoUrl userCount createdIssueCount
|
|
56
|
+
periodUploadVolume samlEnabled scimEnabled createdAt`;
|
|
57
|
+
export const WEBHOOK_FIELDS = `id label url enabled resourceTypes secret
|
|
58
|
+
team { id key } allPublicTeams createdAt`;
|
|
59
|
+
export function buildConnArgs(opts, filterTypeName) {
|
|
60
|
+
const declParts = [];
|
|
61
|
+
const callParts = [`first: $first`];
|
|
62
|
+
const vars = { first: opts.limit ?? 50 };
|
|
63
|
+
declParts.push(`$first: Int`);
|
|
64
|
+
if (filterTypeName && opts.filter !== undefined) {
|
|
65
|
+
declParts.push(`$filter: ${filterTypeName}`);
|
|
66
|
+
callParts.push(`filter: $filter`);
|
|
67
|
+
vars.filter = opts.filter;
|
|
68
|
+
}
|
|
69
|
+
if (opts.includeArchived !== undefined) {
|
|
70
|
+
declParts.push(`$includeArchived: Boolean`);
|
|
71
|
+
callParts.push(`includeArchived: $includeArchived`);
|
|
72
|
+
vars.includeArchived = opts.includeArchived;
|
|
73
|
+
}
|
|
74
|
+
if (opts.orderBy !== undefined) {
|
|
75
|
+
declParts.push(`$orderBy: PaginationOrderBy`);
|
|
76
|
+
callParts.push(`orderBy: $orderBy`);
|
|
77
|
+
vars.orderBy = opts.orderBy;
|
|
78
|
+
}
|
|
79
|
+
if (opts.after !== undefined) {
|
|
80
|
+
declParts.push(`$after: String`);
|
|
81
|
+
callParts.push(`after: $after`);
|
|
82
|
+
vars.after = opts.after;
|
|
83
|
+
}
|
|
84
|
+
if (opts.before !== undefined) {
|
|
85
|
+
declParts.push(`$before: String`);
|
|
86
|
+
callParts.push(`before: $before`);
|
|
87
|
+
vars.before = opts.before;
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
argsDecl: `(${declParts.join(", ")})`,
|
|
91
|
+
argsCall: `(${callParts.join(", ")})`,
|
|
92
|
+
vars,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export const LIST_INPUT_SCHEMA = {
|
|
96
|
+
limit: t.Optional(t.Number({ description: "Max results (default 50, max 250)" })),
|
|
97
|
+
filter: t.Optional(t.Object({}, { description: "Linear filter object (see schema for the resource)" })),
|
|
98
|
+
includeArchived: t.Optional(t.Boolean({ description: "Include archived items" })),
|
|
99
|
+
orderBy: t.Optional(t.String({ description: "createdAt | updatedAt" })),
|
|
100
|
+
after: t.Optional(t.String({ description: "Cursor for forward pagination" })),
|
|
101
|
+
before: t.Optional(t.String({ description: "Cursor for backward pagination" })),
|
|
102
|
+
};
|
|
103
|
+
export function bindListAction(rl) {
|
|
104
|
+
return (...args) => registerListAction(rl, ...args);
|
|
105
|
+
}
|
|
106
|
+
export function bindGetAction(rl) {
|
|
107
|
+
return (...args) => registerGetAction(rl, ...args);
|
|
108
|
+
}
|
|
109
|
+
export function registerListAction(rl, name, description, rootField, filterTypeName, selection) {
|
|
110
|
+
rl.registerAction(name, {
|
|
111
|
+
description,
|
|
112
|
+
inputSchema: t.Object(LIST_INPUT_SCHEMA),
|
|
113
|
+
async execute(input, ctx) {
|
|
114
|
+
const opts = (input ?? {});
|
|
115
|
+
const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
|
|
116
|
+
const data = await gql(key(ctx), `query${argsDecl} { ${rootField}${argsCall} { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }`, vars);
|
|
117
|
+
const conn = data[rootField];
|
|
118
|
+
return { nodes: conn.nodes, pageInfo: conn.pageInfo };
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export function registerGetAction(rl, name, description, rootField, selection) {
|
|
123
|
+
rl.registerAction(name, {
|
|
124
|
+
description,
|
|
125
|
+
inputSchema: t.Object({ id: t.String({ description: "Identifier or slug" }) }),
|
|
126
|
+
async execute(input, ctx) {
|
|
127
|
+
const data = await gql(key(ctx), `query($id: String!) { ${rootField}(id: $id) { ${selection} } }`, { id: input.id });
|
|
128
|
+
return data[rootField];
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { STATE_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
|
|
3
|
+
export function registerStateActions(rl) {
|
|
4
|
+
const listAction = bindListAction(rl);
|
|
5
|
+
const getAction = bindGetAction(rl);
|
|
6
|
+
listAction("state.list", "List workflow states. Filter by team for team-scoped states.", "workflowStates", "WorkflowStateFilter", STATE_FIELDS);
|
|
7
|
+
getAction("state.get", "Get a workflow state by ID.", "workflowState", STATE_FIELDS);
|
|
8
|
+
rl.registerAction("state.create", {
|
|
9
|
+
description: "Create a workflow state in a team.",
|
|
10
|
+
inputSchema: t.Object({
|
|
11
|
+
teamId: t.String({ description: "The team associated with the state" }),
|
|
12
|
+
name: t.String({ description: "The name of the state" }),
|
|
13
|
+
type: t.String({ description: "The workflow state type which categorizes the state. Valid values: backlog, unstarted, started, completed, canceled" }),
|
|
14
|
+
color: t.String({ description: "The color of the state (hex, e.g. #6B7280)" }),
|
|
15
|
+
description: t.Optional(t.String({ description: "The description of the state" })),
|
|
16
|
+
position: t.Optional(t.Number({ description: "The position of the state (Float)" })),
|
|
17
|
+
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
18
|
+
}),
|
|
19
|
+
async execute(input, ctx) {
|
|
20
|
+
const data = await gql(key(ctx), `mutation($input: WorkflowStateCreateInput!) { workflowStateCreate(input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { input: input });
|
|
21
|
+
return data.workflowStateCreate?.workflowState;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
rl.registerAction("state.update", {
|
|
25
|
+
description: "Update a workflow state. Type cannot be changed after creation.",
|
|
26
|
+
inputSchema: t.Object({
|
|
27
|
+
id: t.String({ description: "The identifier of the state to update" }),
|
|
28
|
+
name: t.Optional(t.String({ description: "The name of the state" })),
|
|
29
|
+
color: t.Optional(t.String({ description: "The color of the state (hex)" })),
|
|
30
|
+
description: t.Optional(t.String({ description: "The description of the state" })),
|
|
31
|
+
position: t.Optional(t.Number({ description: "The position of the state (Float)" })),
|
|
32
|
+
}),
|
|
33
|
+
async execute(input, ctx) {
|
|
34
|
+
const { id, ...fields } = input;
|
|
35
|
+
const data = await gql(key(ctx), `mutation($id: String!, $input: WorkflowStateUpdateInput!) { workflowStateUpdate(id: $id, input: $input) { success workflowState { ${STATE_FIELDS} } } }`, { id, input: fields });
|
|
36
|
+
return data.workflowStateUpdate?.workflowState;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { TEAM_FIELDS, USER_FIELDS, bindGetAction, bindListAction, gql, key, } from "./shared.js";
|
|
3
|
+
export function registerTeamActions(rl) {
|
|
4
|
+
const listAction = bindListAction(rl);
|
|
5
|
+
const getAction = bindGetAction(rl);
|
|
6
|
+
listAction("team.list", "List teams whose issues you can access.", "teams", "TeamFilter", TEAM_FIELDS);
|
|
7
|
+
getAction("team.get", "Get a team by ID or key.", "team", TEAM_FIELDS);
|
|
8
|
+
rl.registerAction("team.create", {
|
|
9
|
+
description: "Create a team. Most settings have sensible defaults.",
|
|
10
|
+
inputSchema: t.Object({
|
|
11
|
+
name: t.String({ description: "The name of the team" }),
|
|
12
|
+
key: t.Optional(t.String({ description: "The key of the team. If not given, the key will be generated based on the name" })),
|
|
13
|
+
description: t.Optional(t.String({ description: "The description of the team" })),
|
|
14
|
+
icon: t.Optional(t.String({ description: "The icon of the team" })),
|
|
15
|
+
color: t.Optional(t.String({ description: "The color of the team (hex)" })),
|
|
16
|
+
private: t.Optional(t.Boolean({ description: "Whether the team is private" })),
|
|
17
|
+
timezone: t.Optional(t.String({ description: "The timezone of the team" })),
|
|
18
|
+
cyclesEnabled: t.Optional(t.Boolean({ description: "Whether the team uses cycles" })),
|
|
19
|
+
cycleDuration: t.Optional(t.Number({ description: "The duration of each cycle in weeks (Int)" })),
|
|
20
|
+
cycleCooldownTime: t.Optional(t.Number({ description: "The cooldown time after each cycle in weeks (Int)" })),
|
|
21
|
+
cycleStartDay: t.Optional(t.Number({ description: "The day of the week that a new cycle starts. 0=Sun..6=Sat (Float)" })),
|
|
22
|
+
upcomingCycleCount: t.Optional(t.Number({ description: "How many upcoming cycles to create (Float)" })),
|
|
23
|
+
issueEstimationType: t.Optional(t.String({ description: "The issue estimation type: notUsed | exponential | fibonacci | linear | tShirt" })),
|
|
24
|
+
triageEnabled: t.Optional(t.Boolean({ description: "Whether triage mode is enabled for the team" })),
|
|
25
|
+
parentId: t.Optional(t.String({ description: "The parent team ID (for sub-teams)" })),
|
|
26
|
+
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
27
|
+
copySettingsFromTeamId: t.Optional(t.String({ description: "The team id to copy settings from, if any" })),
|
|
28
|
+
}),
|
|
29
|
+
async execute(input, ctx) {
|
|
30
|
+
const { copySettingsFromTeamId, ...fields } = input;
|
|
31
|
+
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
|
+
return data.teamCreate?.team;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
rl.registerAction("team.update", {
|
|
36
|
+
description: "Update a team. Requires team owner or workspace admin permissions.",
|
|
37
|
+
inputSchema: t.Object({
|
|
38
|
+
id: t.String({ description: "The identifier of the team to update" }),
|
|
39
|
+
name: t.Optional(t.String({ description: "The name of the team" })),
|
|
40
|
+
key: t.Optional(t.String({ description: "The key of the team" })),
|
|
41
|
+
description: t.Optional(t.String({ description: "The description of the team" })),
|
|
42
|
+
icon: t.Optional(t.String({ description: "The icon of the team" })),
|
|
43
|
+
color: t.Optional(t.String({ description: "The color of the team (hex)" })),
|
|
44
|
+
private: t.Optional(t.Boolean({ description: "Whether the team is private" })),
|
|
45
|
+
timezone: t.Optional(t.String({ description: "The timezone of the team" })),
|
|
46
|
+
cyclesEnabled: t.Optional(t.Boolean({ description: "Whether the team uses cycles" })),
|
|
47
|
+
cycleDuration: t.Optional(t.Number({ description: "The duration of each cycle in weeks (Int)" })),
|
|
48
|
+
cycleCooldownTime: t.Optional(t.Number({ description: "The cooldown time after each cycle in weeks (Int)" })),
|
|
49
|
+
cycleStartDay: t.Optional(t.Number({ description: "The day of the week that a new cycle starts. 0=Sun..6=Sat (Float)" })),
|
|
50
|
+
upcomingCycleCount: t.Optional(t.Number({ description: "How many upcoming cycles to create (Float)" })),
|
|
51
|
+
issueEstimationType: t.Optional(t.String({ description: "The issue estimation type: notUsed | exponential | fibonacci | linear | tShirt" })),
|
|
52
|
+
triageEnabled: t.Optional(t.Boolean({ description: "Whether triage mode is enabled for the team" })),
|
|
53
|
+
}),
|
|
54
|
+
async execute(input, ctx) {
|
|
55
|
+
const { id, ...fields } = input;
|
|
56
|
+
const data = await gql(key(ctx), `mutation($id: String!, $input: TeamUpdateInput!) { teamUpdate(id: $id, input: $input) { success team { ${TEAM_FIELDS} } } }`, { id, input: fields });
|
|
57
|
+
return data.teamUpdate?.team;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
rl.registerAction("team.members", {
|
|
61
|
+
description: "List members of a team.",
|
|
62
|
+
inputSchema: t.Object({
|
|
63
|
+
teamId: t.String({ description: "The identifier of the team" }),
|
|
64
|
+
limit: t.Optional(t.Number({ description: "Max members to return (default 50)" })),
|
|
65
|
+
}),
|
|
66
|
+
async execute(input, ctx) {
|
|
67
|
+
const { teamId, limit } = input;
|
|
68
|
+
const data = await gql(key(ctx), `query($id: String!, $first: Int) {
|
|
69
|
+
team(id: $id) { members(first: $first) { nodes { ${USER_FIELDS} } } }
|
|
70
|
+
}`, { id: teamId, first: limit ?? 50 });
|
|
71
|
+
return data.team?.members?.nodes;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { USER_FIELDS, bindGetAction, bindListAction, gql, key } from "./shared.js";
|
|
3
|
+
export function registerUserActions(rl) {
|
|
4
|
+
const listAction = bindListAction(rl);
|
|
5
|
+
const getAction = bindGetAction(rl);
|
|
6
|
+
listAction("user.list", "List users in the workspace.", "users", "UserFilter", USER_FIELDS);
|
|
7
|
+
getAction("user.get", "Get a user by ID. Use 'me' to reference the authenticated user.", "user", USER_FIELDS);
|
|
8
|
+
rl.registerAction("user.me", {
|
|
9
|
+
description: "Get the authenticated user.",
|
|
10
|
+
inputSchema: t.Object({}),
|
|
11
|
+
async execute(_input, ctx) {
|
|
12
|
+
const data = await gql(key(ctx), `query { viewer { ${USER_FIELDS} } }`);
|
|
13
|
+
return data.viewer;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
rl.registerAction("user.update", {
|
|
17
|
+
description: "Update a user. Use id='me' to update the authenticated user.",
|
|
18
|
+
inputSchema: t.Object({
|
|
19
|
+
id: t.String({ description: "The identifier of the user to update. Use 'me' to reference the currently authenticated user" }),
|
|
20
|
+
name: t.Optional(t.String({ description: "The name of the user" })),
|
|
21
|
+
displayName: t.Optional(t.String({ description: "The display name of the user" })),
|
|
22
|
+
description: t.Optional(t.String({ description: "The user description or short bio" })),
|
|
23
|
+
avatarUrl: t.Optional(t.String({ description: "The avatar image URL of the user" })),
|
|
24
|
+
timezone: t.Optional(t.String({ description: "The local timezone of the user" })),
|
|
25
|
+
title: t.Optional(t.String({ description: "The user's job title" })),
|
|
26
|
+
statusEmoji: t.Optional(t.String({ description: "The emoji part of the user status" })),
|
|
27
|
+
statusLabel: t.Optional(t.String({ description: "The label part of the user status" })),
|
|
28
|
+
statusUntilAt: t.Optional(t.String({ description: "When the user status should be cleared (DateTime)" })),
|
|
29
|
+
}),
|
|
30
|
+
async execute(input, ctx) {
|
|
31
|
+
const { id, ...fields } = input;
|
|
32
|
+
const data = await gql(key(ctx), `mutation($id: String!, $input: UserUpdateInput!) { userUpdate(id: $id, input: $input) { success user { ${USER_FIELDS} } } }`, { id, input: fields });
|
|
33
|
+
return data.userUpdate?.user;
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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";
|
|
3
|
+
export function registerViewActions(rl) {
|
|
4
|
+
const listAction = bindListAction(rl);
|
|
5
|
+
const getAction = bindGetAction(rl);
|
|
6
|
+
function customViewConnectionAction(name, description, connectionField, filterTypeName, selection, includeSubTeamsDescription) {
|
|
7
|
+
rl.registerAction(name, {
|
|
8
|
+
description,
|
|
9
|
+
inputSchema: t.Object({
|
|
10
|
+
viewId: t.String({ description: "The custom view ID or slug" }),
|
|
11
|
+
...LIST_INPUT_SCHEMA,
|
|
12
|
+
...(includeSubTeamsDescription
|
|
13
|
+
? { includeSubTeams: t.Optional(t.Boolean({ description: includeSubTeamsDescription })) }
|
|
14
|
+
: {}),
|
|
15
|
+
}),
|
|
16
|
+
async execute(input, ctx) {
|
|
17
|
+
const opts = (input ?? {});
|
|
18
|
+
const { argsDecl, argsCall, vars } = buildConnArgs(opts, filterTypeName);
|
|
19
|
+
const declParts = ["$id: String!", argsDecl.slice(1, -1)];
|
|
20
|
+
const callParts = [argsCall.slice(1, -1)];
|
|
21
|
+
const includeSubTeamsSet = includeSubTeamsDescription !== undefined && opts.includeSubTeams !== undefined;
|
|
22
|
+
if (includeSubTeamsSet) {
|
|
23
|
+
declParts.push("$includeSubTeams: Boolean");
|
|
24
|
+
callParts.push("includeSubTeams: $includeSubTeams");
|
|
25
|
+
vars.includeSubTeams = opts.includeSubTeams;
|
|
26
|
+
}
|
|
27
|
+
const data = await gql(key(ctx), `query(${declParts.join(", ")}) {
|
|
28
|
+
customView(id: $id) { ${connectionField}(${callParts.join(", ")}) { nodes { ${selection} } pageInfo { hasNextPage endCursor } } }
|
|
29
|
+
}`, { id: opts.viewId, ...vars });
|
|
30
|
+
const conn = (data.customView?.[connectionField] ?? {});
|
|
31
|
+
return { nodes: conn.nodes, pageInfo: conn.pageInfo };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
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
|
+
getAction("view.get", "Get a custom view by ID or slug.", "customView", CUSTOM_VIEW_FIELDS);
|
|
37
|
+
rl.registerAction("view.create", {
|
|
38
|
+
description: "Create a custom view. Set filterData for issue views; projectFilterData, initiativeFilterData, or feedItemFilterData for other view types.",
|
|
39
|
+
inputSchema: t.Object({
|
|
40
|
+
name: t.String({ description: "The name of the custom view" }),
|
|
41
|
+
description: t.Optional(t.String({ description: "The description of the custom view" })),
|
|
42
|
+
icon: t.Optional(t.String({ description: "The icon of the custom view" })),
|
|
43
|
+
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" })),
|
|
46
|
+
projectFilterData: t.Optional(t.Object({}, { description: "ProjectFilter for project views" })),
|
|
47
|
+
initiativeFilterData: t.Optional(t.Object({}, { description: "InitiativeFilter for initiative views" })),
|
|
48
|
+
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" })),
|
|
53
|
+
id: t.Optional(t.String({ description: "The identifier in UUID v4 format. If none is provided, the backend will generate one" })),
|
|
54
|
+
}),
|
|
55
|
+
async execute(input, ctx) {
|
|
56
|
+
const data = await gql(key(ctx), `mutation($input: CustomViewCreateInput!) { customViewCreate(input: $input) { success customView { ${CUSTOM_VIEW_FIELDS} } } }`, { input: input });
|
|
57
|
+
return data.customViewCreate?.customView;
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
rl.registerAction("view.update", {
|
|
61
|
+
description: "Update a custom view. All fields optional; only provided fields are updated.",
|
|
62
|
+
inputSchema: t.Object({
|
|
63
|
+
id: t.String({ description: "The identifier of the custom view to update" }),
|
|
64
|
+
name: t.Optional(t.String({ description: "The name of the custom view" })),
|
|
65
|
+
description: t.Optional(t.String({ description: "The description of the custom view" })),
|
|
66
|
+
icon: t.Optional(t.String({ description: "The icon of the custom view" })),
|
|
67
|
+
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" })),
|
|
70
|
+
projectFilterData: t.Optional(t.Object({}, { description: "ProjectFilter for project views" })),
|
|
71
|
+
initiativeFilterData: t.Optional(t.Object({}, { description: "InitiativeFilter for initiative views" })),
|
|
72
|
+
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" })),
|
|
77
|
+
}),
|
|
78
|
+
async execute(input, ctx) {
|
|
79
|
+
const { id, ...fields } = input;
|
|
80
|
+
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
|
+
return data.customViewUpdate?.customView;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
rl.registerAction("view.delete", {
|
|
85
|
+
description: "Delete a custom view.",
|
|
86
|
+
inputSchema: t.Object({ id: t.String({ description: "The identifier of the custom view to delete" }) }),
|
|
87
|
+
async execute(input, ctx) {
|
|
88
|
+
const data = await gql(key(ctx), `mutation($id: String!) { customViewDelete(id: $id) { success } }`, { id: input.id });
|
|
89
|
+
return data.customViewDelete;
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
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");
|
|
93
|
+
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");
|
|
94
|
+
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);
|
|
95
|
+
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");
|
|
96
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as t from "typebox";
|
|
2
|
+
import { WEBHOOK_FIELDS, bindGetAction, bindListAction, gql, key } 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
|
+
const data = await gql(key(ctx), `mutation($input: WebhookCreateInput!) { webhookCreate(input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { input: input });
|
|
22
|
+
return data.webhookCreate?.webhook;
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
rl.registerAction("webhook.update", {
|
|
26
|
+
description: "Update a webhook. teamId and allPublicTeams cannot be changed after creation.",
|
|
27
|
+
inputSchema: t.Object({
|
|
28
|
+
id: t.String({ description: "The identifier of the webhook to update" }),
|
|
29
|
+
url: t.Optional(t.String({ description: "The URL that will be called on data changes" })),
|
|
30
|
+
resourceTypes: t.Optional(t.Array(t.String(), { description: "List of resources the webhook should subscribe to" })),
|
|
31
|
+
label: t.Optional(t.String({ description: "Label for the webhook" })),
|
|
32
|
+
enabled: t.Optional(t.Boolean({ description: "Whether this webhook is enabled" })),
|
|
33
|
+
secret: t.Optional(t.String({ description: "A secret token used to sign the webhook payload" })),
|
|
34
|
+
}),
|
|
35
|
+
async execute(input, ctx) {
|
|
36
|
+
const { id, ...fields } = input;
|
|
37
|
+
const data = await gql(key(ctx), `mutation($id: String!, $input: WebhookUpdateInput!) { webhookUpdate(id: $id, input: $input) { success webhook { ${WEBHOOK_FIELDS} } } }`, { id, input: fields });
|
|
38
|
+
return data.webhookUpdate?.webhook;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
rl.registerAction("webhook.delete", {
|
|
42
|
+
description: "Delete a webhook.",
|
|
43
|
+
inputSchema: t.Object({ id: t.String({ description: "The identifier of the webhook to delete" }) }),
|
|
44
|
+
async execute(input, ctx) {
|
|
45
|
+
const data = await gql(key(ctx), `mutation($id: String!) { webhookDelete(id: $id) { success } }`, { id: input.id });
|
|
46
|
+
return data.webhookDelete;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
rl.registerAction("webhook.rotateSecret", {
|
|
50
|
+
description: "Rotate a webhook's signing secret. Returns the new secret.",
|
|
51
|
+
inputSchema: t.Object({ id: t.String({ description: "The identifier of the webhook to rotate the secret for" }) }),
|
|
52
|
+
async execute(input, ctx) {
|
|
53
|
+
const data = await gql(key(ctx), `mutation($id: String!) { webhookRotateSecret(id: $id) { success secret } }`, { id: input.id });
|
|
54
|
+
return data.webhookRotateSecret;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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
|
}
|