@meetopenbot/linear 0.0.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/index.js ADDED
@@ -0,0 +1,318 @@
1
+ import { agentOutput, definePlugin, shouldHandleInvoke, toolResult, uiWidget, webhookHttpResponse, } from "@meetopenbot/plugin-sdk";
2
+ import { clearTokens, formatMissingCredentials, readLinearConfig, resolveLinearCredentials, resolveWebhookBaseUrl, saveTokens, } from "./config.js";
3
+ import { runLinearAgent } from "./linear-agent.js";
4
+ import { buildIssuesListWidget, isAssignedIssuesPrompt, isListIssuesPrompt, issuesListTitle, } from "./linear-issues.js";
5
+ import { OAUTH_WEBHOOK_PROVIDER, buildOAuthRedirectUri, fetchViewer, handleWebhookOAuthCallback, startOAuthFlow, startWebhookOAuthFlow, } from "./oauth.js";
6
+ /** Client-side action handled by openbot.one — opens `value.url` in the browser. */
7
+ const OPEN_URL_ACTION_ID = "open_url";
8
+ const CONNECT_TOOL = {
9
+ description: "Connect the workspace to Linear via OAuth. Returns an authorization link the user must open in their browser; after they approve, the token is stored automatically. Use this when Linear is not connected yet or credentials expired.",
10
+ inputSchema: { type: "object", properties: {} },
11
+ };
12
+ const DISCONNECT_TOOL = {
13
+ description: "Disconnect Linear: removes the stored OAuth tokens from the workspace.",
14
+ inputSchema: { type: "object", properties: {} },
15
+ };
16
+ const CONNECT_WAIT_MS = 120 * 1000;
17
+ const CONNECT_WIDGET_ID = "linear-connect";
18
+ const CONNECT_PROMPT_WIDGET_ID = "linear-connect-prompt";
19
+ const toolDefinitions = {
20
+ linear_connect: CONNECT_TOOL,
21
+ linear_disconnect: DISCONNECT_TOOL,
22
+ };
23
+ function* emitConnectReply(mode, context, event, data) {
24
+ if (mode === "tool") {
25
+ yield toolResult("linear_connect", event, data);
26
+ return;
27
+ }
28
+ yield agentOutput({
29
+ agentId: context.agentId,
30
+ content: data.output,
31
+ threadId: event.meta?.threadId,
32
+ meta: event.meta,
33
+ });
34
+ }
35
+ function* yieldOAuthAuthorizeWidget(agentId, authorizeUrl, modeHint, options) {
36
+ yield uiWidget({
37
+ agentId,
38
+ threadId: options.threadId,
39
+ meta: options.meta,
40
+ widget: {
41
+ kind: "message",
42
+ widgetId: options.widgetId ?? CONNECT_WIDGET_ID,
43
+ title: "Connect Linear",
44
+ body: `Open Linear in your browser to authorize OpenBot. ${modeHint}`,
45
+ actions: [
46
+ {
47
+ id: OPEN_URL_ACTION_ID,
48
+ label: "Connect Linear",
49
+ variant: "primary",
50
+ value: { url: authorizeUrl, target: "_blank" },
51
+ },
52
+ ],
53
+ },
54
+ });
55
+ }
56
+ async function createOAuthHandle(context, publicBaseUrl) {
57
+ const config = readLinearConfig(context.config ?? {});
58
+ const webhookBaseUrl = resolveWebhookBaseUrl(config, publicBaseUrl);
59
+ if (config.apiKey || !config.clientId)
60
+ return null;
61
+ const scopes = config.scopes ?? "read,write,issues:create,comments:create";
62
+ const handle = webhookBaseUrl
63
+ ? await startWebhookOAuthFlow({
64
+ storage: context.storage,
65
+ clientId: config.clientId,
66
+ clientSecret: config.clientSecret,
67
+ scopes,
68
+ webhookBaseUrl,
69
+ })
70
+ : startOAuthFlow({
71
+ clientId: config.clientId,
72
+ clientSecret: config.clientSecret,
73
+ port: config.oauthPort ?? 4137,
74
+ scopes,
75
+ onSuccess: (tokens) => saveTokens(context.storage, tokens),
76
+ });
77
+ const modeHint = webhookBaseUrl
78
+ ? `After approval, Linear redirects to \`${handle.redirectUri}\`.`
79
+ : "After you approve access you'll be redirected back and can return here.";
80
+ return { handle, modeHint };
81
+ }
82
+ function notConnectedMessage(missing) {
83
+ const needsConnect = missing.includes("accessToken");
84
+ const needsOpenAi = missing.includes("openaiApiKey");
85
+ if (needsConnect && needsOpenAi) {
86
+ return "Linear isn't connected yet, and the OpenAI API key is missing. Connect Linear below, then add `openaiApiKey` to the plugin config.";
87
+ }
88
+ if (needsConnect) {
89
+ return "Linear isn't connected yet. Click **Connect Linear** below to open the authorization page.";
90
+ }
91
+ return formatMissingCredentials(missing);
92
+ }
93
+ async function* handleConnect(context, event, publicBaseUrl, replyMode = "tool") {
94
+ const config = readLinearConfig(context.config ?? {});
95
+ const threadId = event.meta?.threadId;
96
+ const webhookBaseUrl = resolveWebhookBaseUrl(config, publicBaseUrl);
97
+ if (config.apiKey) {
98
+ yield* emitConnectReply(replyMode, context, event, {
99
+ output: "Linear is already configured with an API key in the plugin config — no OAuth connection needed.",
100
+ });
101
+ return;
102
+ }
103
+ const oauth = await createOAuthHandle(context, publicBaseUrl);
104
+ if (!oauth) {
105
+ const webhookHint = webhookBaseUrl
106
+ ? ` set its callback URL to ${buildOAuthRedirectUri(webhookBaseUrl)},`
107
+ : ` set its callback URL to http://localhost:${config.oauthPort}/oauth/callback (local), or configure webhookBaseUrl for ${buildOAuthRedirectUri("https://<your-host>")},`;
108
+ yield* emitConnectReply(replyMode, context, event, {
109
+ output: [
110
+ "Linear OAuth is not configured. Ways to connect:",
111
+ "1. Recommended: open the OpenBot web app → Settings → Plugins and click Connect on the Linear plugin (install-time OAuth, no setup needed).",
112
+ "2. Self-hosted OAuth: create an OAuth application at https://linear.app/settings/api/applications,",
113
+ webhookHint,
114
+ " then put its Client ID into this plugin's `clientId` config field and run linear_connect again.",
115
+ "3. API key: create a personal API key at https://linear.app/settings/api and put it into the `apiKey` config field (or a LINEAR_API_KEY secret variable).",
116
+ ].join("\n"),
117
+ });
118
+ return;
119
+ }
120
+ const { handle, modeHint } = oauth;
121
+ yield* yieldOAuthAuthorizeWidget(context.agentId, handle.authorizeUrl, modeHint, { threadId, meta: event.meta });
122
+ const outcome = await Promise.race([
123
+ handle.completion,
124
+ new Promise((resolve) => {
125
+ const t = setTimeout(() => resolve("pending"), CONNECT_WAIT_MS);
126
+ t.unref?.();
127
+ }),
128
+ ]);
129
+ if (outcome === "pending") {
130
+ yield* emitConnectReply(replyMode, context, event, {
131
+ authorizeUrl: handle.authorizeUrl,
132
+ redirectUri: handle.redirectUri,
133
+ output: "Authorization link sent. Waiting for the user to approve in the browser — the link stays valid for 10 minutes. Once they confirm, ask me to work with Linear again.",
134
+ });
135
+ return;
136
+ }
137
+ if (!outcome) {
138
+ yield* emitConnectReply(replyMode, context, event, {
139
+ error: "authorization_failed",
140
+ output: "Linear authorization did not complete (denied, failed, or the callback is unavailable). Run linear_connect to try again.",
141
+ });
142
+ return;
143
+ }
144
+ let who = "";
145
+ try {
146
+ const { viewer, organization } = await fetchViewer(outcome.accessToken);
147
+ who = ` as ${viewer.displayName ?? viewer.name} in workspace "${organization.name}"`;
148
+ }
149
+ catch {
150
+ // Connection succeeded even if the identity lookup failed.
151
+ }
152
+ yield uiWidget({
153
+ agentId: context.agentId,
154
+ threadId,
155
+ widget: {
156
+ kind: "message",
157
+ widgetId: CONNECT_WIDGET_ID,
158
+ title: "Linear connected",
159
+ body: `✅ Connected to Linear${who}.`,
160
+ state: "submitted",
161
+ },
162
+ });
163
+ yield* emitConnectReply(replyMode, context, event, {
164
+ connected: true,
165
+ output: `Successfully connected to Linear${who}. Tokens are stored securely and refresh automatically.`,
166
+ });
167
+ }
168
+ const linearPluginConfigSchema = {
169
+ type: "object",
170
+ properties: {
171
+ clientId: {
172
+ type: "string",
173
+ description: "Linear OAuth application Client ID (create one at linear.app/settings/api/applications)",
174
+ },
175
+ clientSecret: {
176
+ type: "string",
177
+ description: "Linear OAuth application Client Secret (optional — PKCE is used when omitted)",
178
+ format: "password",
179
+ },
180
+ apiKey: {
181
+ type: "string",
182
+ description: "Linear personal API key (alternative to OAuth)",
183
+ format: "password",
184
+ },
185
+ oauthPort: {
186
+ type: "number",
187
+ description: "Local port for the OAuth callback (must match the OAuth app callback URL)",
188
+ default: 4137,
189
+ },
190
+ scopes: {
191
+ type: "string",
192
+ description: "Comma-separated OAuth scopes",
193
+ default: "read,write,issues:create,comments:create",
194
+ },
195
+ webhookBaseUrl: {
196
+ type: "string",
197
+ description: "Public runtime base URL for OAuth callback via /api/webhooks/linear (e.g. https://my-host.com). Falls back to host publicBaseUrl when omitted.",
198
+ format: "url",
199
+ },
200
+ openaiApiKey: {
201
+ type: "string",
202
+ description: "OpenAI API key for direct Linear agent invocations",
203
+ format: "password",
204
+ },
205
+ model: {
206
+ type: "string",
207
+ description: "OpenAI model id for direct Linear agent invocations (default: gpt-4o-mini)",
208
+ default: "gpt-4o-mini",
209
+ },
210
+ },
211
+ };
212
+ export default definePlugin({
213
+ id: "linear",
214
+ name: "Linear",
215
+ description: "Linear OAuth connect flow and MCP-backed agent for issues, projects, and comments",
216
+ configSchema: linearPluginConfigSchema,
217
+ toolDefinitions,
218
+ factory: (pluginContext) => (builder) => {
219
+ const { agentId, config, storage } = pluginContext;
220
+ const linearConfig = readLinearConfig(config);
221
+ const publicBaseUrl = pluginContext.publicBaseUrl ?? "";
222
+ builder.on("action:webhook", async function* (event) {
223
+ const webhook = event;
224
+ if (webhook.data.provider !== OAUTH_WEBHOOK_PROVIDER)
225
+ return;
226
+ const result = await handleWebhookOAuthCallback({
227
+ storage,
228
+ query: webhook.data.query ?? {},
229
+ onSuccess: (tokens) => saveTokens(storage, tokens),
230
+ });
231
+ if (result.kind === "ignore")
232
+ return;
233
+ yield webhookHttpResponse({
234
+ status: result.status,
235
+ headers: { "Content-Type": "text/html; charset=utf-8" },
236
+ body: result.html,
237
+ });
238
+ });
239
+ builder.on("action:linear_connect", async function* (event) {
240
+ yield* handleConnect(pluginContext, event, publicBaseUrl);
241
+ });
242
+ builder.on("action:linear_disconnect", async function* (event) {
243
+ await clearTokens(storage);
244
+ yield toolResult("linear_disconnect", event, {
245
+ output: "Linear disconnected — stored OAuth tokens were removed.",
246
+ });
247
+ });
248
+ builder.on("agent:invoke", async function* (event, ctx) {
249
+ if (!shouldHandleInvoke(event, agentId))
250
+ return;
251
+ if (event.meta?.threadId) {
252
+ ctx.state.threadId = event.meta.threadId;
253
+ }
254
+ const threadId = event.meta?.threadId ?? ctx.state.threadId;
255
+ const userMessage = (event.data.content ?? "").trim();
256
+ if (!userMessage || !threadId)
257
+ return;
258
+ const auth = await resolveLinearCredentials(linearConfig, storage);
259
+ if (!auth.ok) {
260
+ yield agentOutput({
261
+ agentId,
262
+ content: notConnectedMessage(auth.missing),
263
+ threadId,
264
+ meta: event.meta,
265
+ });
266
+ if (auth.missing.includes("accessToken")) {
267
+ const oauth = await createOAuthHandle(pluginContext, publicBaseUrl);
268
+ if (oauth) {
269
+ yield* yieldOAuthAuthorizeWidget(agentId, oauth.handle.authorizeUrl, oauth.modeHint, {
270
+ threadId,
271
+ meta: event.meta,
272
+ widgetId: CONNECT_PROMPT_WIDGET_ID,
273
+ });
274
+ }
275
+ }
276
+ return;
277
+ }
278
+ try {
279
+ const reply = await runLinearAgent({
280
+ prompt: userMessage,
281
+ openaiApiKey: auth.credentials.openaiApiKey,
282
+ accessToken: auth.credentials.accessToken,
283
+ model: auth.credentials.model,
284
+ });
285
+ const issues = reply.issues;
286
+ const shouldListIssues = isListIssuesPrompt(userMessage) || isAssignedIssuesPrompt(userMessage);
287
+ if (shouldListIssues || issues.length > 0) {
288
+ yield uiWidget({
289
+ agentId,
290
+ threadId,
291
+ meta: event.meta,
292
+ widget: buildIssuesListWidget(issues, {
293
+ title: issuesListTitle(userMessage),
294
+ }),
295
+ });
296
+ }
297
+ const content = reply.toolErrors.length > 0 && !reply.usedTools
298
+ ? `${reply.text}\n\nMCP note: ${reply.toolErrors.join("; ")}`
299
+ : reply.text;
300
+ yield agentOutput({
301
+ agentId,
302
+ content,
303
+ threadId,
304
+ meta: event.meta,
305
+ });
306
+ }
307
+ catch (error) {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ yield agentOutput({
310
+ agentId,
311
+ content: `Linear agent error: ${message}`,
312
+ threadId,
313
+ meta: event.meta,
314
+ });
315
+ }
316
+ });
317
+ },
318
+ });
@@ -0,0 +1,14 @@
1
+ import type { LinearIssue } from "./linear-issues.js";
2
+ export type RunLinearAgentArgs = {
3
+ prompt: string;
4
+ openaiApiKey: string;
5
+ accessToken: string;
6
+ model?: string;
7
+ };
8
+ export type LinearAgentResult = {
9
+ text: string;
10
+ issues: LinearIssue[];
11
+ usedTools: boolean;
12
+ toolErrors: string[];
13
+ };
14
+ export declare function runLinearAgent(args: RunLinearAgentArgs): Promise<LinearAgentResult>;
@@ -0,0 +1,72 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+ import { generateText, stepCountIs } from "ai";
3
+ import { extractIssuesFromToolResults } from "./linear-issues.js";
4
+ import { createLinearMcpClient } from "./linear-mcp.js";
5
+ const SYSTEM_PROMPT = `You are the OpenBot Linear specialist agent.
6
+ You MUST use the provided Linear MCP tools for every request about issues, projects, teams, or comments.
7
+ Never guess or invent Linear data. If a tool returns no results, say so explicitly.
8
+ For listing or searching issues, call linear_get_user first when the user asks for issues assigned to them, then call linear_search_issues with the correct filters.
9
+ Summarize tool results clearly in plain text. Be concise and friendly.`;
10
+ function extractToolErrors(toolResults) {
11
+ const errors = [];
12
+ for (const toolResult of toolResults) {
13
+ const output = toolResult.output;
14
+ if (!output || typeof output !== "object")
15
+ continue;
16
+ const record = output;
17
+ if (typeof record.error === "string") {
18
+ errors.push(`${toolResult.toolName}: ${record.error}`);
19
+ continue;
20
+ }
21
+ if (typeof record.isError === "boolean" && record.isError) {
22
+ const text = typeof record.text === "string"
23
+ ? record.text
24
+ : JSON.stringify(record).slice(0, 300);
25
+ errors.push(`${toolResult.toolName}: ${text}`);
26
+ }
27
+ }
28
+ return errors;
29
+ }
30
+ export async function runLinearAgent(args) {
31
+ const openai = createOpenAI({ apiKey: args.openaiApiKey });
32
+ const mcpClient = await createLinearMcpClient({
33
+ accessToken: args.accessToken,
34
+ });
35
+ try {
36
+ const tools = await mcpClient.tools();
37
+ if (Object.keys(tools).length === 0) {
38
+ throw new Error("Linear MCP server returned no tools. Check that mcp-server-linear is installed and reachable.");
39
+ }
40
+ const result = await generateText({
41
+ model: openai(args.model ?? "gpt-4o-mini"),
42
+ system: SYSTEM_PROMPT,
43
+ prompt: args.prompt,
44
+ tools,
45
+ stopWhen: stepCountIs(10),
46
+ });
47
+ const toolResults = result.steps.flatMap((step) => step.toolResults.map((toolResult) => ({
48
+ toolName: toolResult.toolName,
49
+ output: toolResult.output,
50
+ })));
51
+ const issues = extractIssuesFromToolResults(toolResults);
52
+ const toolErrors = extractToolErrors(toolResults);
53
+ const text = result.text.trim();
54
+ if (!text && toolErrors.length > 0) {
55
+ return {
56
+ text: `Linear tool error: ${toolErrors[0]}`,
57
+ issues,
58
+ usedTools: toolResults.length > 0,
59
+ toolErrors,
60
+ };
61
+ }
62
+ return {
63
+ text: text || (issues.length > 0 ? `Found ${issues.length} issue(s).` : "Done."),
64
+ issues,
65
+ usedTools: toolResults.length > 0,
66
+ toolErrors,
67
+ };
68
+ }
69
+ finally {
70
+ await mcpClient.close();
71
+ }
72
+ }
@@ -0,0 +1,33 @@
1
+ import type { RenderUIWidgetData } from "@meetopenbot/plugin-sdk";
2
+ export type LinearIssue = {
3
+ id: string;
4
+ identifier: string;
5
+ title: string;
6
+ url: string;
7
+ state?: {
8
+ name: string;
9
+ type: string;
10
+ };
11
+ assignee?: {
12
+ name: string;
13
+ };
14
+ team?: {
15
+ name: string;
16
+ key: string;
17
+ };
18
+ project?: {
19
+ name: string;
20
+ };
21
+ };
22
+ export declare const ISSUES_LIST_WIDGET_ID = "linear-issues-list";
23
+ export declare function buildIssuesListWidget(issues: LinearIssue[], options?: {
24
+ title?: string;
25
+ description?: string;
26
+ }): RenderUIWidgetData;
27
+ export declare function extractIssuesFromToolResults(toolResults: Array<{
28
+ toolName: string;
29
+ output: unknown;
30
+ }>): LinearIssue[];
31
+ export declare function isListIssuesPrompt(message: string): boolean;
32
+ export declare function isAssignedIssuesPrompt(message: string): boolean;
33
+ export declare function issuesListTitle(prompt: string): string;
@@ -0,0 +1,136 @@
1
+ const OPEN_URL_ACTION_ID = "open_url";
2
+ export const ISSUES_LIST_WIDGET_ID = "linear-issues-list";
3
+ function mapStateStatus(stateType) {
4
+ switch (stateType) {
5
+ case "started":
6
+ return "in_progress";
7
+ case "completed":
8
+ return "done";
9
+ case "canceled":
10
+ return "cancelled";
11
+ case "backlog":
12
+ case "unstarted":
13
+ return "pending";
14
+ default:
15
+ return undefined;
16
+ }
17
+ }
18
+ export function buildIssuesListWidget(issues, options) {
19
+ return {
20
+ kind: "list",
21
+ widgetId: ISSUES_LIST_WIDGET_ID,
22
+ title: options?.title ?? "Linear issues",
23
+ description: options?.description ??
24
+ (issues.length === 0
25
+ ? "No issues matched this query."
26
+ : `${issues.length} issue${issues.length === 1 ? "" : "s"}`),
27
+ items: issues.map((issue) => {
28
+ const details = [
29
+ issue.state?.name,
30
+ issue.assignee?.name ? `Assignee: ${issue.assignee.name}` : undefined,
31
+ issue.team?.name,
32
+ issue.project?.name,
33
+ ]
34
+ .filter(Boolean)
35
+ .join(" · ");
36
+ return {
37
+ id: issue.id,
38
+ label: `${issue.identifier}: ${issue.title}`,
39
+ description: details || undefined,
40
+ badge: issue.state?.name,
41
+ status: mapStateStatus(issue.state?.type),
42
+ actions: issue.url
43
+ ? [
44
+ {
45
+ id: OPEN_URL_ACTION_ID,
46
+ label: "Open",
47
+ variant: "secondary",
48
+ value: { url: issue.url, target: "_blank" },
49
+ },
50
+ ]
51
+ : undefined,
52
+ metadata: {
53
+ identifier: issue.identifier,
54
+ url: issue.url,
55
+ },
56
+ };
57
+ }),
58
+ };
59
+ }
60
+ function extractTextPayload(value) {
61
+ if (typeof value === "string")
62
+ return value;
63
+ if (!value || typeof value !== "object")
64
+ return undefined;
65
+ const record = value;
66
+ if (typeof record.text === "string")
67
+ return record.text;
68
+ if (Array.isArray(record.content)) {
69
+ for (const part of record.content) {
70
+ if (part &&
71
+ typeof part === "object" &&
72
+ part.type === "text" &&
73
+ typeof part.text === "string") {
74
+ return part.text;
75
+ }
76
+ }
77
+ }
78
+ return undefined;
79
+ }
80
+ function parseIssuesPayload(payload) {
81
+ if (!payload || typeof payload !== "object")
82
+ return [];
83
+ const root = payload;
84
+ const issuesNode = root.issues ??
85
+ root.data?.issues;
86
+ if (!issuesNode?.nodes || !Array.isArray(issuesNode.nodes))
87
+ return [];
88
+ return issuesNode.nodes
89
+ .filter((node) => {
90
+ if (!node || typeof node !== "object")
91
+ return false;
92
+ const issue = node;
93
+ return (typeof issue.id === "string" &&
94
+ typeof issue.identifier === "string" &&
95
+ typeof issue.title === "string");
96
+ })
97
+ .map((issue) => ({
98
+ ...issue,
99
+ url: typeof issue.url === "string" ? issue.url : "",
100
+ }));
101
+ }
102
+ export function extractIssuesFromToolResults(toolResults) {
103
+ const byId = new Map();
104
+ for (const toolResult of toolResults) {
105
+ if (!toolResult.toolName.includes("search_issues"))
106
+ continue;
107
+ const text = extractTextPayload(toolResult.output);
108
+ if (!text)
109
+ continue;
110
+ try {
111
+ for (const issue of parseIssuesPayload(JSON.parse(text))) {
112
+ byId.set(issue.id, issue);
113
+ }
114
+ }
115
+ catch {
116
+ // Ignore malformed tool payloads.
117
+ }
118
+ }
119
+ return [...byId.values()];
120
+ }
121
+ export function isListIssuesPrompt(message) {
122
+ const normalized = message.trim().toLowerCase();
123
+ if (!normalized)
124
+ return false;
125
+ return (/\b(list|show|get|fetch|what are|available)\b/.test(normalized) &&
126
+ /\bissues?\b/.test(normalized));
127
+ }
128
+ export function isAssignedIssuesPrompt(message) {
129
+ const normalized = message.trim().toLowerCase();
130
+ return (/\b(assigned to me|my issues|issues for me|issues assigned)\b/.test(normalized) || /\bissues?\s+assigned\b/.test(normalized));
131
+ }
132
+ export function issuesListTitle(prompt) {
133
+ return isAssignedIssuesPrompt(prompt)
134
+ ? "Issues assigned to you"
135
+ : "Linear issues";
136
+ }
@@ -0,0 +1,4 @@
1
+ export type LinearMcpClientArgs = {
2
+ accessToken: string;
3
+ };
4
+ export declare function createLinearMcpClient(args: LinearMcpClientArgs): Promise<import("@ai-sdk/mcp").MCPClient>;
@@ -0,0 +1,26 @@
1
+ import { createRequire } from "node:module";
2
+ import { createMCPClient } from "@ai-sdk/mcp";
3
+ import { Experimental_StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio";
4
+ const require = createRequire(import.meta.url);
5
+ function resolveMcpServerLinearLaunch() {
6
+ try {
7
+ const entry = require.resolve("mcp-server-linear");
8
+ return { command: process.execPath, args: [entry] };
9
+ }
10
+ catch {
11
+ return { command: "npx", args: ["-y", "mcp-server-linear"] };
12
+ }
13
+ }
14
+ export async function createLinearMcpClient(args) {
15
+ const launch = resolveMcpServerLinearLaunch();
16
+ return createMCPClient({
17
+ transport: new Experimental_StdioMCPTransport({
18
+ command: launch.command,
19
+ args: launch.args,
20
+ env: {
21
+ ...process.env,
22
+ LINEAR_ACCESS_TOKEN: args.accessToken,
23
+ },
24
+ }),
25
+ });
26
+ }
@@ -0,0 +1,13 @@
1
+ import type { Storage } from "@meetopenbot/plugin-sdk";
2
+ export declare const VAR_OAUTH_PENDING = "LINEAR_OAUTH_PENDING";
3
+ export interface PendingOAuthSession {
4
+ state: string;
5
+ codeVerifier: string;
6
+ clientId: string;
7
+ clientSecret?: string;
8
+ redirectUri: string;
9
+ expiresAt: number;
10
+ }
11
+ export declare function savePendingOAuthSession(storage: Storage, session: PendingOAuthSession): Promise<void>;
12
+ export declare function loadPendingOAuthSession(storage: Storage): Promise<PendingOAuthSession | null>;
13
+ export declare function clearPendingOAuthSession(storage: Storage): Promise<void>;
@@ -0,0 +1,40 @@
1
+ export const VAR_OAUTH_PENDING = "LINEAR_OAUTH_PENDING";
2
+ function variableValue(variables, key) {
3
+ const entry = variables[key];
4
+ if (typeof entry === "string")
5
+ return entry || undefined;
6
+ return entry?.value || undefined;
7
+ }
8
+ export async function savePendingOAuthSession(storage, session) {
9
+ await storage.createVariable({
10
+ key: VAR_OAUTH_PENDING,
11
+ value: JSON.stringify(session),
12
+ secret: true,
13
+ });
14
+ }
15
+ export async function loadPendingOAuthSession(storage) {
16
+ const variables = (await storage.getVariables().catch(() => ({})));
17
+ const raw = variableValue(variables, VAR_OAUTH_PENDING) ??
18
+ process.env[VAR_OAUTH_PENDING];
19
+ if (!raw)
20
+ return null;
21
+ try {
22
+ const session = JSON.parse(raw);
23
+ if (typeof session.state !== "string" ||
24
+ typeof session.codeVerifier !== "string" ||
25
+ typeof session.clientId !== "string" ||
26
+ typeof session.redirectUri !== "string" ||
27
+ typeof session.expiresAt !== "number") {
28
+ return null;
29
+ }
30
+ if (Date.now() > session.expiresAt)
31
+ return null;
32
+ return session;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ export async function clearPendingOAuthSession(storage) {
39
+ await storage.deleteVariable({ key: VAR_OAUTH_PENDING }).catch(() => { });
40
+ }