@meetopenbot/linear 0.0.2 → 0.0.4

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.
@@ -1,136 +0,0 @@
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
- }
@@ -1,4 +0,0 @@
1
- export type LinearMcpClientArgs = {
2
- accessToken: string;
3
- };
4
- export declare function createLinearMcpClient(args: LinearMcpClientArgs): Promise<import("@ai-sdk/mcp").MCPClient>;
@@ -1,13 +0,0 @@
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>;
@@ -1,40 +0,0 @@
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
- }
package/dist/oauth.d.ts DELETED
@@ -1,95 +0,0 @@
1
- /**
2
- * Linear OAuth 2.0 (authorization code + PKCE).
3
- *
4
- * Two callback modes:
5
- * - Loopback: temporary localhost server (local runtimes).
6
- * - Webhook: redirect to `https://<host>/api/webhooks/linear` (cloud/remote).
7
- */
8
- import type { Storage } from "@meetopenbot/plugin-sdk";
9
- export declare const LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
10
- export declare const LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
11
- export declare const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
12
- export declare const OAUTH_WEBHOOK_PROVIDER = "linear";
13
- export interface OAuthTokens {
14
- accessToken: string;
15
- refreshToken?: string;
16
- /** Epoch ms when the access token expires, if Linear reported expiry. */
17
- expiresAt?: number;
18
- scope?: string;
19
- }
20
- export interface StartOAuthFlowArgs {
21
- clientId: string;
22
- clientSecret?: string;
23
- port: number;
24
- scopes: string;
25
- /** Called once the code has been exchanged successfully. */
26
- onSuccess: (tokens: OAuthTokens) => Promise<void>;
27
- onError?: (error: Error) => void;
28
- /** How long the callback server stays alive, in ms. */
29
- timeoutMs?: number;
30
- }
31
- export interface OAuthFlowHandle {
32
- authorizeUrl: string;
33
- redirectUri: string;
34
- /** Resolves with tokens on success, null on timeout/cancel. */
35
- completion: Promise<OAuthTokens | null>;
36
- cancel: () => void;
37
- }
38
- export interface StartWebhookOAuthFlowArgs {
39
- storage: Storage;
40
- clientId: string;
41
- clientSecret?: string;
42
- scopes: string;
43
- webhookBaseUrl: string;
44
- timeoutMs?: number;
45
- }
46
- export declare function oauthHtmlPage(title: string, body: string, ok: boolean): string;
47
- export declare function buildOAuthRedirectUri(webhookBaseUrl: string): string;
48
- export declare function buildAuthorizeUrl(args: {
49
- clientId: string;
50
- redirectUri: string;
51
- scopes: string;
52
- state: string;
53
- codeChallenge: string;
54
- }): string;
55
- export declare function exchangeAuthorizationCode(args: {
56
- code: string;
57
- redirectUri: string;
58
- clientId: string;
59
- clientSecret?: string;
60
- codeVerifier: string;
61
- }): Promise<OAuthTokens>;
62
- export declare function refreshAccessToken(args: {
63
- refreshToken: string;
64
- clientId: string;
65
- clientSecret?: string;
66
- }): Promise<OAuthTokens>;
67
- export declare function startWebhookOAuthFlow(args: StartWebhookOAuthFlowArgs): Promise<OAuthFlowHandle>;
68
- export type WebhookOAuthCallbackResult = {
69
- kind: "oauth";
70
- status: number;
71
- html: string;
72
- tokens: OAuthTokens | null;
73
- state?: string;
74
- } | {
75
- kind: "ignore";
76
- };
77
- export declare function handleWebhookOAuthCallback(args: {
78
- storage: Storage;
79
- query: Record<string, unknown>;
80
- onSuccess: (tokens: OAuthTokens) => Promise<void>;
81
- }): Promise<WebhookOAuthCallbackResult>;
82
- export declare function startOAuthFlow(args: StartOAuthFlowArgs): OAuthFlowHandle;
83
- export declare function fetchViewer(accessToken: string): Promise<{
84
- viewer: {
85
- id: string;
86
- name: string;
87
- displayName: string;
88
- email: string;
89
- };
90
- organization: {
91
- id: string;
92
- name: string;
93
- urlKey: string;
94
- };
95
- }>;
package/dist/oauth.js DELETED
@@ -1,372 +0,0 @@
1
- /**
2
- * Linear OAuth 2.0 (authorization code + PKCE).
3
- *
4
- * Two callback modes:
5
- * - Loopback: temporary localhost server (local runtimes).
6
- * - Webhook: redirect to `https://<host>/api/webhooks/linear` (cloud/remote).
7
- */
8
- import { createHash, randomBytes } from "node:crypto";
9
- import { createServer } from "node:http";
10
- import { clearPendingOAuthSession, loadPendingOAuthSession, savePendingOAuthSession, } from "./oauth-pending.js";
11
- export const LINEAR_AUTHORIZE_URL = "https://linear.app/oauth/authorize";
12
- export const LINEAR_TOKEN_URL = "https://api.linear.app/oauth/token";
13
- export const LINEAR_GRAPHQL_URL = "https://api.linear.app/graphql";
14
- export const OAUTH_WEBHOOK_PROVIDER = "linear";
15
- const LOOPBACK_CALLBACK_PATH = "/oauth/callback";
16
- const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
17
- const PENDING_SESSION_MS = 10 * 60 * 1000;
18
- let activeServer = null;
19
- const webhookOAuthCompletions = new Map();
20
- function base64url(buffer) {
21
- return buffer
22
- .toString("base64")
23
- .replace(/\+/g, "-")
24
- .replace(/\//g, "_")
25
- .replace(/=+$/, "");
26
- }
27
- export function oauthHtmlPage(title, body, ok) {
28
- return `<!doctype html>
29
- <html>
30
- <head>
31
- <meta charset="utf-8" />
32
- <meta name="viewport" content="width=device-width, initial-scale=1" />
33
- <title>${title}</title>
34
- <style>
35
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; display: flex; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #101012; color: #ededef; }
36
- .card { text-align: center; padding: 48px 56px; border-radius: 16px; background: #1b1b1f; border: 1px solid #2a2a30; max-width: 420px; }
37
- .icon { font-size: 44px; margin-bottom: 16px; }
38
- h1 { font-size: 20px; margin: 0 0 8px; }
39
- p { color: #9b9ba3; margin: 0; line-height: 1.5; }
40
- </style>
41
- </head>
42
- <body>
43
- <div class="card">
44
- <div class="icon">${ok ? "✅" : "⚠️"}</div>
45
- <h1>${title}</h1>
46
- <p>${body}</p>
47
- </div>
48
- </body>
49
- </html>`;
50
- }
51
- export function buildOAuthRedirectUri(webhookBaseUrl) {
52
- const base = webhookBaseUrl.replace(/\/$/, "");
53
- return `${base}/api/webhooks/${OAUTH_WEBHOOK_PROVIDER}`;
54
- }
55
- function generatePkce() {
56
- const state = base64url(randomBytes(24));
57
- const codeVerifier = base64url(randomBytes(48));
58
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
59
- return { state, codeVerifier, codeChallenge };
60
- }
61
- export function buildAuthorizeUrl(args) {
62
- return (`${LINEAR_AUTHORIZE_URL}?` +
63
- new URLSearchParams({
64
- client_id: args.clientId,
65
- redirect_uri: args.redirectUri,
66
- response_type: "code",
67
- scope: args.scopes,
68
- state: args.state,
69
- prompt: "consent",
70
- code_challenge: args.codeChallenge,
71
- code_challenge_method: "S256",
72
- }).toString());
73
- }
74
- export async function exchangeAuthorizationCode(args) {
75
- const body = new URLSearchParams({
76
- grant_type: "authorization_code",
77
- code: args.code,
78
- redirect_uri: args.redirectUri,
79
- client_id: args.clientId,
80
- code_verifier: args.codeVerifier,
81
- });
82
- if (args.clientSecret)
83
- body.set("client_secret", args.clientSecret);
84
- const response = await fetch(LINEAR_TOKEN_URL, {
85
- method: "POST",
86
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
87
- body,
88
- });
89
- const payload = (await response.json().catch(() => ({})));
90
- if (!response.ok || typeof payload.access_token !== "string") {
91
- const detail = typeof payload.error_description === "string"
92
- ? payload.error_description
93
- : JSON.stringify(payload);
94
- throw new Error(`Linear token exchange failed (${response.status}): ${detail}`);
95
- }
96
- return normalizeTokenResponse(payload);
97
- }
98
- export async function refreshAccessToken(args) {
99
- const body = new URLSearchParams({
100
- grant_type: "refresh_token",
101
- refresh_token: args.refreshToken,
102
- client_id: args.clientId,
103
- });
104
- if (args.clientSecret)
105
- body.set("client_secret", args.clientSecret);
106
- const response = await fetch(LINEAR_TOKEN_URL, {
107
- method: "POST",
108
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
109
- body,
110
- });
111
- const payload = (await response.json().catch(() => ({})));
112
- if (!response.ok || typeof payload.access_token !== "string") {
113
- const detail = typeof payload.error_description === "string"
114
- ? payload.error_description
115
- : JSON.stringify(payload);
116
- throw new Error(`Linear token refresh failed (${response.status}): ${detail}`);
117
- }
118
- return normalizeTokenResponse(payload);
119
- }
120
- function normalizeTokenResponse(payload) {
121
- return {
122
- accessToken: payload.access_token,
123
- refreshToken: typeof payload.refresh_token === "string"
124
- ? payload.refresh_token
125
- : undefined,
126
- expiresAt: typeof payload.expires_in === "number"
127
- ? Date.now() + payload.expires_in * 1000
128
- : undefined,
129
- scope: Array.isArray(payload.scope)
130
- ? payload.scope.join(",")
131
- : payload.scope,
132
- };
133
- }
134
- function registerWebhookOAuthCompletion(state, timeoutMs) {
135
- return new Promise((resolve) => {
136
- const existing = webhookOAuthCompletions.get(state);
137
- if (existing) {
138
- webhookOAuthCompletions.delete(state);
139
- }
140
- const timeout = setTimeout(() => {
141
- webhookOAuthCompletions.delete(state);
142
- resolve(null);
143
- }, timeoutMs);
144
- timeout.unref?.();
145
- webhookOAuthCompletions.set(state, (tokens) => {
146
- clearTimeout(timeout);
147
- resolve(tokens);
148
- });
149
- });
150
- }
151
- function settleWebhookOAuthCompletion(state, tokens) {
152
- const resolve = webhookOAuthCompletions.get(state);
153
- if (!resolve)
154
- return;
155
- webhookOAuthCompletions.delete(state);
156
- resolve(tokens);
157
- }
158
- export async function startWebhookOAuthFlow(args) {
159
- const { state, codeVerifier, codeChallenge } = generatePkce();
160
- const redirectUri = buildOAuthRedirectUri(args.webhookBaseUrl);
161
- const pending = {
162
- state,
163
- codeVerifier,
164
- clientId: args.clientId,
165
- clientSecret: args.clientSecret,
166
- redirectUri,
167
- expiresAt: Date.now() + PENDING_SESSION_MS,
168
- };
169
- await savePendingOAuthSession(args.storage, pending);
170
- const authorizeUrl = buildAuthorizeUrl({
171
- clientId: args.clientId,
172
- redirectUri,
173
- scopes: args.scopes,
174
- state,
175
- codeChallenge,
176
- });
177
- const timeoutMs = args.timeoutMs ?? DEFAULT_TIMEOUT_MS;
178
- const completion = registerWebhookOAuthCompletion(state, timeoutMs);
179
- return {
180
- authorizeUrl,
181
- redirectUri,
182
- completion,
183
- cancel: () => {
184
- settleWebhookOAuthCompletion(state, null);
185
- void clearPendingOAuthSession(args.storage);
186
- },
187
- };
188
- }
189
- export async function handleWebhookOAuthCallback(args) {
190
- const code = queryParam(args.query, "code");
191
- const state = queryParam(args.query, "state");
192
- const oauthError = queryParam(args.query, "error");
193
- if (!code && !state && !oauthError) {
194
- return { kind: "ignore" };
195
- }
196
- if (oauthError) {
197
- if (state)
198
- settleWebhookOAuthCompletion(state, null);
199
- await clearPendingOAuthSession(args.storage);
200
- return {
201
- kind: "oauth",
202
- status: 400,
203
- html: oauthHtmlPage("Connection failed", `Linear returned: ${oauthError}. Return to OpenBot and try again.`, false),
204
- tokens: null,
205
- state,
206
- };
207
- }
208
- const pending = await loadPendingOAuthSession(args.storage);
209
- if (!pending || !code || !state || pending.state !== state) {
210
- return {
211
- kind: "oauth",
212
- status: 400,
213
- html: oauthHtmlPage("Connection failed", "Invalid callback (missing code or state mismatch). Return to OpenBot and try again.", false),
214
- tokens: null,
215
- state,
216
- };
217
- }
218
- try {
219
- const tokens = await exchangeAuthorizationCode({
220
- code,
221
- redirectUri: pending.redirectUri,
222
- clientId: pending.clientId,
223
- clientSecret: pending.clientSecret,
224
- codeVerifier: pending.codeVerifier,
225
- });
226
- await args.onSuccess(tokens);
227
- await clearPendingOAuthSession(args.storage);
228
- settleWebhookOAuthCompletion(state, tokens);
229
- return {
230
- kind: "oauth",
231
- status: 200,
232
- html: oauthHtmlPage("Connected to Linear", "You can close this tab and return to OpenBot.", true),
233
- tokens,
234
- state,
235
- };
236
- }
237
- catch (error) {
238
- const message = error instanceof Error ? error.message : String(error);
239
- await clearPendingOAuthSession(args.storage);
240
- settleWebhookOAuthCompletion(state, null);
241
- return {
242
- kind: "oauth",
243
- status: 500,
244
- html: oauthHtmlPage("Connection failed", `${message}. Return to OpenBot and try again.`, false),
245
- tokens: null,
246
- state,
247
- };
248
- }
249
- }
250
- function queryParam(query, key) {
251
- const value = query[key];
252
- if (typeof value === "string" && value.trim())
253
- return value.trim();
254
- if (Array.isArray(value) && typeof value[0] === "string") {
255
- return value[0].trim();
256
- }
257
- return undefined;
258
- }
259
- export function startOAuthFlow(args) {
260
- if (activeServer) {
261
- activeServer.close();
262
- activeServer = null;
263
- }
264
- const { state, codeVerifier, codeChallenge } = generatePkce();
265
- const redirectUri = `http://localhost:${args.port}${LOOPBACK_CALLBACK_PATH}`;
266
- const authorizeUrl = buildAuthorizeUrl({
267
- clientId: args.clientId,
268
- redirectUri,
269
- scopes: args.scopes,
270
- state,
271
- codeChallenge,
272
- });
273
- let settle;
274
- const completion = new Promise((resolve) => {
275
- settle = resolve;
276
- });
277
- const server = createServer(async (req, res) => {
278
- const url = new URL(req.url ?? "/", `http://localhost:${args.port}`);
279
- if (url.pathname !== LOOPBACK_CALLBACK_PATH) {
280
- res.writeHead(404).end();
281
- return;
282
- }
283
- const finish = (status, title, body, ok) => {
284
- res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" });
285
- res.end(oauthHtmlPage(title, body, ok));
286
- };
287
- const error = url.searchParams.get("error");
288
- if (error) {
289
- finish(400, "Connection failed", `Linear returned: ${error}. Return to OpenBot and try again.`, false);
290
- cleanup();
291
- args.onError?.(new Error(`Linear authorization failed: ${error}`));
292
- settle(null);
293
- return;
294
- }
295
- const code = url.searchParams.get("code");
296
- if (!code || url.searchParams.get("state") !== state) {
297
- finish(400, "Connection failed", "Invalid callback (missing code or state mismatch). Return to OpenBot and try again.", false);
298
- return;
299
- }
300
- try {
301
- const tokens = await exchangeAuthorizationCode({
302
- code,
303
- redirectUri,
304
- clientId: args.clientId,
305
- clientSecret: args.clientSecret,
306
- codeVerifier,
307
- });
308
- await args.onSuccess(tokens);
309
- finish(200, "Connected to Linear", "You can close this tab and return to OpenBot.", true);
310
- cleanup();
311
- settle(tokens);
312
- }
313
- catch (error) {
314
- const message = error instanceof Error ? error.message : String(error);
315
- finish(500, "Connection failed", `${message}. Return to OpenBot and try again.`, false);
316
- cleanup();
317
- args.onError?.(error instanceof Error ? error : new Error(message));
318
- settle(null);
319
- }
320
- });
321
- const timeout = setTimeout(() => {
322
- cleanup();
323
- settle(null);
324
- }, args.timeoutMs ?? DEFAULT_TIMEOUT_MS);
325
- timeout.unref?.();
326
- function cleanup() {
327
- clearTimeout(timeout);
328
- if (activeServer === server)
329
- activeServer = null;
330
- server.close();
331
- }
332
- server.on("error", (error) => {
333
- cleanup();
334
- args.onError?.(error);
335
- settle(null);
336
- });
337
- server.listen(args.port);
338
- activeServer = server;
339
- return {
340
- authorizeUrl,
341
- redirectUri,
342
- completion,
343
- cancel: () => {
344
- cleanup();
345
- settle(null);
346
- },
347
- };
348
- }
349
- export async function fetchViewer(accessToken) {
350
- const response = await fetch(LINEAR_GRAPHQL_URL, {
351
- method: "POST",
352
- headers: {
353
- "Content-Type": "application/json",
354
- Authorization: `Bearer ${accessToken}`,
355
- },
356
- body: JSON.stringify({
357
- query: `query { viewer { id name displayName email } organization { id name urlKey } }`,
358
- }),
359
- });
360
- if (!response.ok) {
361
- const body = await response.text().catch(() => "");
362
- throw new Error(`Linear API request failed (${response.status}): ${body.slice(0, 500)}`);
363
- }
364
- const payload = (await response.json());
365
- if (payload.errors?.length) {
366
- throw new Error(payload.errors.map((e) => e.message).join("; "));
367
- }
368
- if (!payload.data) {
369
- throw new Error("Linear API returned no data.");
370
- }
371
- return payload.data;
372
- }
package/dist/tools.d.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Linear tool implementations. Each tool returns structured `data` for the
3
- * model plus a human-readable `output` summary, and optionally a UI widget
4
- * spec rendered in the OpenBot client.
5
- */
6
- import type { RenderUIWidgetData, ToolDefinition } from '@meetopenbot/plugin-sdk';
7
- import { type LinearAuth } from './api.js';
8
- export interface ToolRunResult {
9
- data: unknown;
10
- output: string;
11
- widget?: RenderUIWidgetData;
12
- }
13
- export interface LinearTool {
14
- definition: ToolDefinition;
15
- run: (auth: LinearAuth, args: Record<string, unknown>) => Promise<ToolRunResult>;
16
- }
17
- export declare const linearTools: Record<string, LinearTool>;