@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/README.md +79 -0
- package/dist/api.d.ts +17 -0
- package/dist/api.js +29 -0
- package/dist/auth.d.ts +33 -0
- package/dist/auth.js +91 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +152 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +318 -0
- package/dist/linear-agent.d.ts +14 -0
- package/dist/linear-agent.js +72 -0
- package/dist/linear-issues.d.ts +33 -0
- package/dist/linear-issues.js +136 -0
- package/dist/linear-mcp.d.ts +4 -0
- package/dist/linear-mcp.js +26 -0
- package/dist/oauth-pending.d.ts +13 -0
- package/dist/oauth-pending.js +40 -0
- package/dist/oauth.d.ts +95 -0
- package/dist/oauth.js +372 -0
- package/dist/tools.d.ts +17 -0
- package/dist/tools.js +290 -0
- package/package.json +41 -0
- package/src/config.ts +228 -0
- package/src/index.ts +449 -0
- package/src/linear-agent.ts +107 -0
- package/src/linear-issues.ts +188 -0
- package/src/linear-mcp.ts +33 -0
- package/src/oauth-pending.ts +68 -0
- package/src/oauth.ts +563 -0
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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>;
|