@sentry/junior-linear 0.121.0 → 0.122.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/README.md +1 -1
- package/dist/index.js +21 -210
- package/dist/plugin.d.ts +1 -1
- package/package.json +2 -2
- package/skills/linear/SKILL.md +1 -1
- package/skills/linear/references/api-surface.md +2 -2
- package/dist/tools/create-issue.d.ts +0 -40
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ export const plugins = defineJuniorPlugins([linearPlugin()]);
|
|
|
19
19
|
|
|
20
20
|
This package does not require a shared `LINEAR_API_KEY` or a custom OAuth app for the default setup. Each user connects their own Linear account the first time Junior calls a Linear MCP tool. Junior sends the authorization link privately and resumes the same Slack thread automatically after the user authorizes.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Linear operations use Linear's hosted MCP tools directly. When an issue is created through that path, Junior links it to the current conversation.
|
|
23
23
|
|
|
24
24
|
Optional: set channel defaults when a Slack thread usually routes work to the same Linear destination:
|
|
25
25
|
|
package/dist/index.js
CHANGED
|
@@ -2,220 +2,36 @@
|
|
|
2
2
|
import {
|
|
3
3
|
defineJuniorPlugin
|
|
4
4
|
} from "@sentry/junior-plugin-api";
|
|
5
|
-
|
|
6
|
-
// src/tools/create-issue.ts
|
|
7
|
-
import {
|
|
8
|
-
definePluginTool,
|
|
9
|
-
PluginToolInputError,
|
|
10
|
-
pluginToolResultSchema
|
|
11
|
-
} from "@sentry/junior-plugin-api";
|
|
12
5
|
import { z } from "zod";
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}).strict();
|
|
22
|
-
var issueSchema = z.object({
|
|
23
|
-
id: z.string().optional(),
|
|
24
|
-
identifier: z.string().optional(),
|
|
25
|
-
title: z.string().optional(),
|
|
26
|
-
url: z.string().url().optional()
|
|
27
|
-
}).strict();
|
|
28
|
-
var outputSchema = pluginToolResultSchema.extend({
|
|
29
|
-
ok: z.literal(true),
|
|
30
|
-
status: z.literal("success"),
|
|
31
|
-
target: z.literal("createIssue"),
|
|
32
|
-
data: z.object({
|
|
33
|
-
issue: issueSchema.nullable(),
|
|
34
|
-
providerText: z.string()
|
|
35
|
-
}),
|
|
36
|
-
issue: issueSchema.nullable(),
|
|
37
|
-
providerText: z.string()
|
|
38
|
-
});
|
|
39
|
-
var completedStateSchema = z.object({
|
|
40
|
-
createdAtMs: z.number(),
|
|
41
|
-
issue: issueSchema.nullable(),
|
|
42
|
-
providerText: z.string(),
|
|
43
|
-
status: z.literal("completed")
|
|
44
|
-
}).strict();
|
|
45
|
-
var pendingStateSchema = z.object({
|
|
46
|
-
createdAtMs: z.number(),
|
|
47
|
-
status: z.literal("pending")
|
|
48
|
-
}).strict();
|
|
49
|
-
var stateSchema = z.union([completedStateSchema, pendingStateSchema]);
|
|
50
|
-
function parseState(value) {
|
|
51
|
-
const parsed = stateSchema.safeParse(value);
|
|
52
|
-
return parsed.success ? parsed.data : void 0;
|
|
53
|
-
}
|
|
54
|
-
function collectObjects(value, objects) {
|
|
55
|
-
if (!value || typeof value !== "object") {
|
|
6
|
+
var saveIssueResultSchema = z.object({
|
|
7
|
+
issue: z.object({
|
|
8
|
+
identifier: z.string().trim().min(1),
|
|
9
|
+
url: z.url()
|
|
10
|
+
}).passthrough()
|
|
11
|
+
}).passthrough();
|
|
12
|
+
async function annotateCreatedIssue(ctx) {
|
|
13
|
+
if (ctx.tool.name !== "save_issue" || ctx.tool.arguments.id !== void 0) {
|
|
56
14
|
return;
|
|
57
15
|
}
|
|
58
|
-
if (
|
|
59
|
-
for (const item of value) collectObjects(item, objects);
|
|
16
|
+
if (!ctx.annotations) {
|
|
60
17
|
return;
|
|
61
18
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
for (const object of objects) {
|
|
68
|
-
const value = object[field];
|
|
69
|
-
if (typeof value === "string" && value.trim()) return value.trim();
|
|
70
|
-
}
|
|
71
|
-
return void 0;
|
|
72
|
-
}
|
|
73
|
-
function providerText(result) {
|
|
74
|
-
return result.content.filter(
|
|
75
|
-
(part) => part.type === "text"
|
|
76
|
-
).map((part) => part.text).join("\n\n").trim();
|
|
77
|
-
}
|
|
78
|
-
function parseJsonText(text) {
|
|
79
|
-
try {
|
|
80
|
-
return JSON.parse(text);
|
|
81
|
-
} catch {
|
|
82
|
-
return void 0;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function extractIssue(result) {
|
|
86
|
-
const text = providerText(result);
|
|
87
|
-
const objects = [];
|
|
88
|
-
collectObjects(result.structuredContent, objects);
|
|
89
|
-
collectObjects(parseJsonText(text), objects);
|
|
90
|
-
const urlMatch = text.match(
|
|
91
|
-
/https:\/\/linear\.app\/[^\s<>)"']+\/issue\/([A-Z][A-Z0-9]*-\d+)[^\s<>)"']*/i
|
|
92
|
-
);
|
|
93
|
-
const identifierMatch = text.match(/\b[A-Z][A-Z0-9]*-\d+\b/);
|
|
94
|
-
const issue = {
|
|
95
|
-
id: stringField(objects, "id"),
|
|
96
|
-
identifier: stringField(objects, "identifier") ?? urlMatch?.[1]?.toUpperCase() ?? identifierMatch?.[0],
|
|
97
|
-
title: stringField(objects, "title"),
|
|
98
|
-
url: stringField(objects, "url") ?? urlMatch?.[0]
|
|
99
|
-
};
|
|
100
|
-
const parsed = issueSchema.safeParse(
|
|
101
|
-
Object.fromEntries(
|
|
102
|
-
Object.entries(issue).filter((entry) => entry[1] !== void 0)
|
|
103
|
-
)
|
|
104
|
-
);
|
|
105
|
-
if (!parsed.success || Object.keys(parsed.data).length === 0) {
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
return parsed.data;
|
|
109
|
-
}
|
|
110
|
-
function toolResult(issue, text) {
|
|
111
|
-
const data = { issue, providerText: text };
|
|
112
|
-
return {
|
|
113
|
-
ok: true,
|
|
114
|
-
status: "success",
|
|
115
|
-
target: "createIssue",
|
|
116
|
-
data,
|
|
117
|
-
...data
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async function annotateIssue(ctx, issue) {
|
|
121
|
-
if (!issue?.identifier || !issue.url) {
|
|
19
|
+
const result = saveIssueResultSchema.safeParse(ctx.result.structuredContent);
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
ctx.log.warn("linear.issue_annotation.skipped", {
|
|
22
|
+
"app.reason": "unexpected_save_response"
|
|
23
|
+
});
|
|
122
24
|
return;
|
|
123
25
|
}
|
|
124
|
-
|
|
26
|
+
const identifier = result.data.issue.identifier.toUpperCase();
|
|
27
|
+
await ctx.annotations.upsert({
|
|
125
28
|
kind: "resource_link",
|
|
126
|
-
key:
|
|
127
|
-
label:
|
|
128
|
-
url: issue.url,
|
|
29
|
+
key: identifier,
|
|
30
|
+
label: identifier,
|
|
31
|
+
url: result.data.issue.url,
|
|
129
32
|
status: "open"
|
|
130
33
|
});
|
|
131
34
|
}
|
|
132
|
-
function required(value, name) {
|
|
133
|
-
if (!value?.trim()) throw new PluginToolInputError(`${name} is required`);
|
|
134
|
-
return value.trim();
|
|
135
|
-
}
|
|
136
|
-
function createLinearIssueTool(ctx) {
|
|
137
|
-
return definePluginTool({
|
|
138
|
-
description: "Create a Linear issue through Linear's hosted MCP provider and link the created issue to the current Junior conversation.",
|
|
139
|
-
inputSchema,
|
|
140
|
-
outputSchema,
|
|
141
|
-
annotations: {
|
|
142
|
-
destructiveHint: false,
|
|
143
|
-
idempotentHint: true,
|
|
144
|
-
openWorldHint: true,
|
|
145
|
-
readOnlyHint: false,
|
|
146
|
-
title: "Create Linear issue"
|
|
147
|
-
},
|
|
148
|
-
describeProposal(input) {
|
|
149
|
-
return `Create a Linear issue in ${input.team}: ${input.title}`;
|
|
150
|
-
},
|
|
151
|
-
async execute(input, options) {
|
|
152
|
-
const mcp = ctx.mcp;
|
|
153
|
-
if (!mcp) {
|
|
154
|
-
throw new Error("Linear MCP provider is unavailable.");
|
|
155
|
-
}
|
|
156
|
-
const conversationId = required(ctx.conversationId, "conversationId");
|
|
157
|
-
const toolCallId = required(options.toolCallId, "toolCallId");
|
|
158
|
-
if (await mcp.prepare() === "authorization_pending") {
|
|
159
|
-
return toolResult(null, "Authorization pending.");
|
|
160
|
-
}
|
|
161
|
-
const key = `createIssue:${conversationId}:${toolCallId}`;
|
|
162
|
-
return await ctx.state.withLock(
|
|
163
|
-
`${key}:lock`,
|
|
164
|
-
CREATE_ISSUE_LOCK_TTL_MS,
|
|
165
|
-
async () => {
|
|
166
|
-
const state = parseState(await ctx.state.get(key));
|
|
167
|
-
if (state?.status === "completed") {
|
|
168
|
-
await annotateIssue(ctx, state.issue);
|
|
169
|
-
return toolResult(state.issue, state.providerText);
|
|
170
|
-
}
|
|
171
|
-
if (state?.status === "pending") {
|
|
172
|
-
throw new Error(
|
|
173
|
-
"Linear issue creation for this tool call has an uncertain pending result; refusing to create a duplicate issue."
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
await ctx.state.set(
|
|
177
|
-
key,
|
|
178
|
-
{ status: "pending", createdAtMs: Date.now() },
|
|
179
|
-
CREATE_ISSUE_STATE_TTL_MS
|
|
180
|
-
);
|
|
181
|
-
const result = await mcp.callTool({
|
|
182
|
-
name: "create_issue",
|
|
183
|
-
arguments: input,
|
|
184
|
-
toolCallId
|
|
185
|
-
});
|
|
186
|
-
if (result.status === "authorization_pending") {
|
|
187
|
-
await ctx.state.delete(key);
|
|
188
|
-
return toolResult(null, "Authorization pending.");
|
|
189
|
-
}
|
|
190
|
-
if (result.status === "error") {
|
|
191
|
-
await ctx.state.delete(key);
|
|
192
|
-
throw new Error(result.message);
|
|
193
|
-
}
|
|
194
|
-
const text = providerText(result);
|
|
195
|
-
const issue = extractIssue(result);
|
|
196
|
-
const completedState = {
|
|
197
|
-
status: "completed",
|
|
198
|
-
createdAtMs: Date.now(),
|
|
199
|
-
issue,
|
|
200
|
-
providerText: text
|
|
201
|
-
};
|
|
202
|
-
try {
|
|
203
|
-
await ctx.state.set(key, completedState, CREATE_ISSUE_STATE_TTL_MS);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
throw new Error(
|
|
206
|
-
"Linear issue was created, but Junior could not persist the completed issue state.",
|
|
207
|
-
{ cause: error }
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
await annotateIssue(ctx, issue);
|
|
211
|
-
return toolResult(issue, text);
|
|
212
|
-
}
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// src/plugin.ts
|
|
219
35
|
function linearPlugin() {
|
|
220
36
|
return defineJuniorPlugin({
|
|
221
37
|
packageName: "@sentry/junior-linear",
|
|
@@ -225,17 +41,12 @@ function linearPlugin() {
|
|
|
225
41
|
displayName: "Linear",
|
|
226
42
|
mcp: {
|
|
227
43
|
transport: "http",
|
|
228
|
-
url: "https://mcp.linear.app/mcp"
|
|
229
|
-
wrappedTools: ["create_issue"]
|
|
44
|
+
url: "https://mcp.linear.app/mcp"
|
|
230
45
|
},
|
|
231
46
|
name: "linear"
|
|
232
47
|
},
|
|
233
48
|
hooks: {
|
|
234
|
-
|
|
235
|
-
return {
|
|
236
|
-
createIssue: createLinearIssueTool(ctx)
|
|
237
|
-
};
|
|
238
|
-
}
|
|
49
|
+
afterMcpTool: annotateCreatedIssue
|
|
239
50
|
}
|
|
240
51
|
});
|
|
241
52
|
}
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type PluginRegistration } from "@sentry/junior-plugin-api";
|
|
2
|
-
/** Register Linear's hosted MCP provider and
|
|
2
|
+
/** Register Linear's hosted MCP provider and conversation-link side effects. */
|
|
3
3
|
export declare function linearPlugin(): PluginRegistration;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sentry/junior-linear",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.122.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"zod": "^4.4.3",
|
|
26
|
-
"@sentry/junior-plugin-api": "0.
|
|
26
|
+
"@sentry/junior-plugin-api": "0.122.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^25.9.1",
|
package/skills/linear/SKILL.md
CHANGED
|
@@ -67,7 +67,7 @@ If any gate fails, revise and re-check before calling the Linear create/update t
|
|
|
67
67
|
|
|
68
68
|
6. Execute:
|
|
69
69
|
|
|
70
|
-
- Create
|
|
70
|
+
- Create or update issues with Linear's live hosted MCP tools. Discover the current create/update tool and copy only fields justified by its live schema.
|
|
71
71
|
- For updates, prefer partial changes over full rewrites. Fetch current issue state first if the mutation could overwrite structured fields or duplicate an existing comment.
|
|
72
72
|
- Check for duplicates silently before creating a new issue when the request appears related to existing work.
|
|
73
73
|
|
|
@@ -21,8 +21,8 @@ The current public docs describe support for finding, creating, and updating obj
|
|
|
21
21
|
| Intent | Minimum tool pattern |
|
|
22
22
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
23
23
|
| Inspect an issue | Resolve the issue by key, URL, or search query, then fetch current state before answering. |
|
|
24
|
-
| Create an issue | Confirm the team first, then create the
|
|
25
|
-
| Update fields | Fetch current issue state first, then mutate only the requested fields. |
|
|
24
|
+
| Create an issue | Confirm the team first, then create via the live hosted MCP tools with grounded title/body content and only fields justified by the current schema. |
|
|
25
|
+
| Update fields | Fetch current issue state first, then mutate via the live hosted MCP tools with only the requested fields. |
|
|
26
26
|
| Add a comment | Resolve the exact issue first, then add a concise comment with durable links and next steps. |
|
|
27
27
|
| Move state or assign | Read the current issue and team workflow first when state, workflow, or assignee ambiguity could cause the wrong mutation. |
|
|
28
28
|
| Check for duplicates | Search for an existing matching issue before opening a new one when the request appears related to ongoing work. |
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { type ToolRegistrationHookContext } from "@sentry/junior-plugin-api";
|
|
2
|
-
/** Create a Linear issue through the hosted MCP provider and link it to the conversation. */
|
|
3
|
-
export declare function createLinearIssueTool(ctx: ToolRegistrationHookContext): import("@sentry/junior-plugin-api").PluginToolDefinition<{
|
|
4
|
-
team: string;
|
|
5
|
-
title: string;
|
|
6
|
-
description?: string | undefined;
|
|
7
|
-
priority?: "low" | "medium" | "high" | "urgent" | undefined;
|
|
8
|
-
project?: string | undefined;
|
|
9
|
-
}, {
|
|
10
|
-
[x: string]: unknown;
|
|
11
|
-
ok: true;
|
|
12
|
-
status: "success";
|
|
13
|
-
target: "createIssue";
|
|
14
|
-
data: {
|
|
15
|
-
issue: {
|
|
16
|
-
id?: string | undefined;
|
|
17
|
-
identifier?: string | undefined;
|
|
18
|
-
title?: string | undefined;
|
|
19
|
-
url?: string | undefined;
|
|
20
|
-
} | null;
|
|
21
|
-
providerText: string;
|
|
22
|
-
};
|
|
23
|
-
issue: {
|
|
24
|
-
id?: string | undefined;
|
|
25
|
-
identifier?: string | undefined;
|
|
26
|
-
title?: string | undefined;
|
|
27
|
-
url?: string | undefined;
|
|
28
|
-
} | null;
|
|
29
|
-
providerText: string;
|
|
30
|
-
truncated?: boolean | undefined;
|
|
31
|
-
continuation?: {
|
|
32
|
-
arguments: Record<string, unknown>;
|
|
33
|
-
reason?: string | undefined;
|
|
34
|
-
} | undefined;
|
|
35
|
-
error?: string | {
|
|
36
|
-
kind: string;
|
|
37
|
-
message: string;
|
|
38
|
-
retryable?: boolean | undefined;
|
|
39
|
-
} | undefined;
|
|
40
|
-
}>;
|