Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/runline-plugins/linear/src/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import {
gql,
key,
requireUnscoped,
withScopedNote,
} from "./shared.js";

export function registerAttachmentActions(rl: RunlinePluginAPI) {
rl.registerAction("attachment.list", {
access: "read",
description:
"List issue attachments. Disabled for scoped Linear connections.",
description: withScopedNote("List issue attachments."),
inputSchema: t.Object({ limit: t.Optional(t.Number()) }),
async execute(input, ctx) {
requireUnscoped(ctx, "attachment.list");
Expand Down
13 changes: 8 additions & 5 deletions packages/runline-plugins/linear/src/cycles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
gql,
key,
requireUnscoped,
withScopedNote,
} from "./shared.js";

export function registerCycleActions(rl: RunlinePluginAPI) {
Expand All @@ -15,15 +16,15 @@ export function registerCycleActions(rl: RunlinePluginAPI) {

listAction(
"cycle.list",
"List cycles. Use filter for isActive/isNext/isPrevious.",
"List cycles. The current cycle is filter { isActive: { eq: true } }; isNext and isPrevious work the same way. Combine with issue.list to report on a cycle's work.",
"cycles",
"CycleFilter",
CYCLE_FIELDS,
);
getAction("cycle.get", "Get a cycle by ID.", "cycle", CYCLE_FIELDS);
rl.registerAction("cycle.create", {
access: "write",
description: "Create a cycle for a team.",
description: withScopedNote("Create a cycle for a team."),
inputSchema: t.Object({
teamId: t.String({ description: "The team to associate the cycle with" }),
startsAt: t.String({
Expand Down Expand Up @@ -63,7 +64,7 @@ export function registerCycleActions(rl: RunlinePluginAPI) {
});
rl.registerAction("cycle.update", {
access: "write",
description: "Update a cycle.",
description: withScopedNote("Update a cycle."),
inputSchema: t.Object({
id: t.String({ description: "The identifier of the cycle to update" }),
name: t.Optional(
Expand Down Expand Up @@ -105,8 +106,9 @@ export function registerCycleActions(rl: RunlinePluginAPI) {
// they are not archived with it.
rl.registerAction("cycle.archive", {
access: "write",
description:
description: withScopedNote(
"Archive one cycle. Issues assigned to it are unlinked from the cycle first, not archived. This is the per-cycle removal; Linear has no single-cycle delete.",
),
inputSchema: t.Object({
id: t.String({ description: "The identifier of the cycle to archive" }),
}),
Expand All @@ -132,8 +134,9 @@ export function registerCycleActions(rl: RunlinePluginAPI) {
// team, and the description leads with the blast radius.
rl.registerAction("team.cyclesDeleteAll", {
access: "write",
description:
description: withScopedNote(
"DESTRUCTIVE: delete ALL cycle data for a team and disable the cycles feature. Removes every cycle and its issue associations, not just one. To remove a single cycle use cycle.archive.",
),
inputSchema: t.Object({
teamId: t.String({
description:
Expand Down
4 changes: 2 additions & 2 deletions packages/runline-plugins/linear/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { registerWebhookActions } from "./webhooks.js";

export default function linear(rl: RunlinePluginAPI) {
rl.setName("linear");
rl.setVersion("0.4.0");
rl.setVersion("0.5.0");
rl.setConnectionSchema(
t.Object({
apiKey: t.String({
Expand All @@ -27,7 +27,7 @@ export default function linear(rl: RunlinePluginAPI) {
scopeLabelIds: t.Optional(
t.String({
description:
"Comma-separated Linear issue label IDs. When set, issue/comment/attachment access is restricted to issues with one of these labels.",
"Comma-separated Linear issue label UUIDs (label names also accepted and resolved). When set, issue/comment/attachment access is restricted to issues with one of these labels; workspace metadata such as teams, states, labels, cycles and users stays readable.",
env: "LINEAR_SCOPE_LABEL_IDS",
}),
),
Expand Down
15 changes: 10 additions & 5 deletions packages/runline-plugins/linear/src/initiatives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
INITIATIVE_FIELDS,
key,
requireUnscoped,
withScopedNote,
} from "./shared.js";

export function registerInitiativeActions(rl: RunlinePluginAPI) {
Expand All @@ -28,7 +29,9 @@ export function registerInitiativeActions(rl: RunlinePluginAPI) {
);
rl.registerAction("initiative.create", {
access: "write",
description: "Create an initiative. Status: Planned | Active | Completed.",
description: withScopedNote(
"Create an initiative. Status: Planned | Active | Completed.",
),
inputSchema: t.Object({
name: t.String({ description: "The name of the initiative" }),
description: t.Optional(
Expand Down Expand Up @@ -89,7 +92,7 @@ export function registerInitiativeActions(rl: RunlinePluginAPI) {
});
rl.registerAction("initiative.update", {
access: "write",
description: "Update an initiative.",
description: withScopedNote("Update an initiative."),
inputSchema: t.Object({
id: t.String({
description: "The identifier of the initiative to update",
Expand Down Expand Up @@ -154,7 +157,7 @@ export function registerInitiativeActions(rl: RunlinePluginAPI) {
});
rl.registerAction("initiative.delete", {
access: "write",
description: "Trash an initiative.",
description: withScopedNote("Trash an initiative."),
inputSchema: t.Object({
id: t.String({
description: "The identifier of the initiative to delete",
Expand All @@ -172,8 +175,9 @@ export function registerInitiativeActions(rl: RunlinePluginAPI) {
});
rl.registerAction("initiative.addProject", {
access: "write",
description:
description: withScopedNote(
"Associate a project with an initiative. Use this action for project-to-initiative linking; project.update does not accept initiativeId. Verify with initiative.get or the returned initiative.projects list.",
),
inputSchema: t.Object({
initiativeId: t.String({
description: "The identifier of the initiative",
Expand Down Expand Up @@ -204,8 +208,9 @@ export function registerInitiativeActions(rl: RunlinePluginAPI) {
});
rl.registerAction("initiative.removeProject", {
access: "write",
description:
description: withScopedNote(
"Remove a project from an initiative. Pass the link id returned by initiative.addProject, then verify with initiative.get.",
),
inputSchema: t.Object({
id: t.String({
description: "The identifier of the initiativeToProject to delete",
Expand Down
23 changes: 12 additions & 11 deletions packages/runline-plugins/linear/src/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
}),
async execute(input, ctx) {
const fields = { ...(input as Record<string, unknown>) };
fields.labelIds = ensureScopeLabelsOnCreateOrReplace(
fields.labelIds = await ensureScopeLabelsOnCreateOrReplace(
ctx,
fields.labelIds,
);
Expand All @@ -153,7 +153,7 @@ export function registerIssueActions(rl: RunlinePluginAPI) {

rl.registerAction("issue.get", {
access: "read",
description: "Get an issue by ID or identifier (e.g. 'THE-154')",
description: "Get an issue by ID or identifier (e.g. 'THE-154').",
inputSchema: t.Object({
issueId: t.String({
description:
Expand All @@ -167,7 +167,7 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
{ id: (input as { issueId: string }).issueId },
);
const issue = data.issue;
if (!issueHasScope(ctx, issue))
if (!(await issueHasScope(ctx, issue)))
throw new Error(
"Linear issue is not available to this scoped connection",
);
Expand All @@ -178,7 +178,7 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
rl.registerAction("issue.list", {
access: "read",
description:
"List issues. Pass `filter` for state/label/project/etc. Default hides archived.",
"List issues. teamId and assigneeId are convenience filters; use `filter` for anything else - assignee { assignee: { isMe: { eq: true } } }, cycle { cycle: { id: { eq: 'cycle-id' } } }, open work { state: { type: { nin: ['completed','canceled'] } } }. Default hides archived.",
inputSchema: t.Object(
{
...LIST_INPUT_SCHEMA,
Expand All @@ -200,7 +200,7 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
const merged: Record<string, unknown> = { ...(opts.filter ?? {}) };
if (opts.teamId) merged.team = { id: { eq: opts.teamId } };
if (opts.assigneeId) merged.assignee = { id: { eq: opts.assigneeId } };
const filter = mergeIssueScopeFilter(
const filter = await mergeIssueScopeFilter(
ctx,
Object.keys(merged).length > 0 ? merged : undefined,
);
Expand Down Expand Up @@ -332,9 +332,9 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
const { issueId, ...fields } = input as Record<string, unknown>;
await assertIssueInScope(ctx, String(issueId));
if (fields.removedLabelIds)
forbidScopeLabelRemoval(ctx, fields.removedLabelIds);
await forbidScopeLabelRemoval(ctx, fields.removedLabelIds);
if (fields.labelIds)
fields.labelIds = ensureScopeLabelsOnCreateOrReplace(
fields.labelIds = await ensureScopeLabelsOnCreateOrReplace(
ctx,
fields.labelIds,
);
Expand Down Expand Up @@ -455,10 +455,10 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
term: opts.term,
first: opts.limit ?? 50,
filter:
mergeIssueScopeFilter(
(await mergeIssueScopeFilter(
ctx,
opts.filter as Record<string, unknown> | undefined,
) ?? null,
)) ?? null,
includeComments: opts.includeComments ?? null,
includeArchived: opts.includeArchived ?? null,
teamId: opts.teamId ?? null,
Expand Down Expand Up @@ -506,7 +506,7 @@ export function registerIssueActions(rl: RunlinePluginAPI) {
labelId: string;
};
await assertIssueInScope(ctx, issueId);
forbidScopeLabelRemoval(ctx, labelId);
await forbidScopeLabelRemoval(ctx, labelId);
const data = await gql(
key(ctx),
`mutation($id: String!, $labelId: String!) { issueRemoveLabel(id: $id, labelId: $labelId) { success } }`,
Expand Down Expand Up @@ -594,7 +594,8 @@ export function registerIssueActions(rl: RunlinePluginAPI) {

rl.registerAction("issue.listComments", {
access: "read",
description: "List comments on an issue.",
description:
"List comments on an issue. Equivalent to comment.list({ issueId }); prefer that action when you also need workspace-wide listing.",
inputSchema: t.Object({
issueId: t.String(),
limit: t.Optional(t.Number()),
Expand Down
14 changes: 9 additions & 5 deletions packages/runline-plugins/linear/src/labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
key,
LABEL_FIELDS,
requireUnscoped,
withScopedNote,
} from "./shared.js";

export function registerLabelActions(rl: RunlinePluginAPI) {
Expand All @@ -23,7 +24,9 @@ export function registerLabelActions(rl: RunlinePluginAPI) {
getAction("label.get", "Get a label by ID.", "issueLabel", LABEL_FIELDS);
rl.registerAction("label.create", {
access: "write",
description: "Create a label. Omit teamId for a workspace-level label.",
description: withScopedNote(
"Create a label. Omit teamId for a workspace-level label.",
),
inputSchema: t.Object({
name: t.String({ description: "The name of the label" }),
teamId: t.Optional(
Expand Down Expand Up @@ -78,7 +81,7 @@ export function registerLabelActions(rl: RunlinePluginAPI) {
});
rl.registerAction("label.update", {
access: "write",
description: "Update a label.",
description: withScopedNote("Update a label."),
inputSchema: t.Object({
id: t.String({ description: "The identifier of the label to update" }),
name: t.Optional(t.String({ description: "The name of the label" })),
Expand Down Expand Up @@ -123,7 +126,7 @@ export function registerLabelActions(rl: RunlinePluginAPI) {
});
rl.registerAction("label.delete", {
access: "write",
description: "Delete a label.",
description: withScopedNote("Delete a label."),
inputSchema: t.Object({
id: t.String({ description: "The identifier of the label to delete" }),
}),
Expand All @@ -139,8 +142,9 @@ export function registerLabelActions(rl: RunlinePluginAPI) {
});
rl.registerAction("label.retire", {
access: "write",
description:
description: withScopedNote(
"Retire a label. Retired labels remain visible but cannot be applied to new issues.",
),
inputSchema: t.Object({
id: t.String({ description: "The identifier of the label to retire" }),
}),
Expand All @@ -156,7 +160,7 @@ export function registerLabelActions(rl: RunlinePluginAPI) {
});
rl.registerAction("label.restore", {
access: "write",
description: "Restore a previously retired label.",
description: withScopedNote("Restore a previously retired label."),
inputSchema: t.Object({
id: t.String({ description: "The identifier of the label to restore" }),
}),
Expand Down
10 changes: 8 additions & 2 deletions packages/runline-plugins/linear/src/organization.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import type { RunlinePluginAPI } from "runline";
import * as t from "typebox";
import { gql, key, ORG_FIELDS, requireUnscoped } from "./shared.js";
import {
gql,
key,
ORG_FIELDS,
requireUnscoped,
withScopedNote,
} from "./shared.js";

export function registerOrganizationActions(rl: RunlinePluginAPI) {
rl.registerAction("org.get", {
access: "read",
description: "Get the authenticated workspace.",
description: withScopedNote("Get the authenticated workspace."),
inputSchema: t.Object({}),
async execute(_input, ctx) {
requireUnscoped(ctx, "org.get");
Expand Down
Loading