@meetopenbot/linear 0.0.3 → 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.
package/src/config.ts DELETED
@@ -1,230 +0,0 @@
1
- import type { Storage } from "@meetopenbot/plugin-sdk";
2
- import { refreshAccessToken, type OAuthTokens } from "./oauth.js";
3
-
4
- export const VAR_API_KEY = "LINEAR_API_KEY";
5
- export const VAR_ACCESS_TOKEN = "LINEAR_ACCESS_TOKEN";
6
- export const VAR_REFRESH_TOKEN = "LINEAR_REFRESH_TOKEN";
7
- export const VAR_TOKEN_EXPIRES_AT = "LINEAR_TOKEN_EXPIRES_AT";
8
- export const VAR_CLIENT_ID = "LINEAR_CLIENT_ID";
9
- export const GO_BACK_TO_OPENBOT_URL = "https://openbot.one/settings/agents";
10
- export const LINEAR_OPENBOT_CLIENT_ID = "1263fc2dbbf08efa7b2c4b1b2b2582be";
11
-
12
- const REFRESH_MARGIN_MS = 5 * 60 * 1000;
13
-
14
- export type LinearPluginConfig = {
15
- clientId?: string;
16
- clientSecret?: string;
17
- apiKey?: string;
18
- oauthPort?: number;
19
- scopes?: string;
20
- openaiApiKey?: string;
21
- model?: string;
22
- /** Public runtime base URL, e.g. https://my-host.com (used for OAuth webhook callback). */
23
- webhookBaseUrl?: string;
24
- };
25
-
26
- export type LinearCredentials = {
27
- accessToken: string;
28
- openaiApiKey: string;
29
- model: string;
30
- clientId?: string;
31
- clientSecret?: string;
32
- };
33
-
34
- type VariableValue = string | { value: string; secret: boolean } | undefined;
35
-
36
- function variableValue(
37
- variables: Record<string, VariableValue>,
38
- key: string,
39
- ): string | undefined {
40
- const entry = variables[key];
41
- if (typeof entry === "string") return entry || undefined;
42
- return entry?.value || undefined;
43
- }
44
-
45
- export function readLinearConfig(
46
- config: Record<string, unknown>,
47
- ): LinearPluginConfig {
48
- return {
49
- clientId:
50
- typeof config.clientId === "string" && config.clientId.trim()
51
- ? config.clientId.trim()
52
- : LINEAR_OPENBOT_CLIENT_ID,
53
- clientSecret:
54
- typeof config.clientSecret === "string" && config.clientSecret.trim()
55
- ? config.clientSecret.trim()
56
- : undefined,
57
- apiKey:
58
- typeof config.apiKey === "string" && config.apiKey.trim()
59
- ? config.apiKey.trim()
60
- : undefined,
61
- oauthPort: typeof config.oauthPort === "number" ? config.oauthPort : 4137,
62
- scopes:
63
- typeof config.scopes === "string" && config.scopes.trim()
64
- ? config.scopes.trim()
65
- : "read,write,issues:create,comments:create",
66
- openaiApiKey:
67
- typeof config.openaiApiKey === "string" && config.openaiApiKey.trim()
68
- ? config.openaiApiKey.trim()
69
- : undefined,
70
- model:
71
- typeof config.model === "string" && config.model.trim()
72
- ? config.model.trim()
73
- : undefined,
74
- webhookBaseUrl:
75
- typeof config.webhookBaseUrl === "string" && config.webhookBaseUrl.trim()
76
- ? config.webhookBaseUrl.trim()
77
- : undefined,
78
- };
79
- }
80
-
81
- export function resolveWebhookBaseUrl(
82
- config: LinearPluginConfig,
83
- publicBaseUrl?: string,
84
- ): string | undefined {
85
- const explicit = config.webhookBaseUrl?.replace(/\/$/, "");
86
- if (explicit) return explicit;
87
- const host = publicBaseUrl?.trim().replace(/\/$/, "");
88
- if (host) return host;
89
- return undefined;
90
- }
91
-
92
- export async function saveTokens(
93
- storage: Storage,
94
- tokens: OAuthTokens,
95
- ): Promise<void> {
96
- await storage.createVariable({
97
- key: VAR_ACCESS_TOKEN,
98
- value: tokens.accessToken,
99
- secret: true,
100
- });
101
- if (tokens.refreshToken) {
102
- await storage.createVariable({
103
- key: VAR_REFRESH_TOKEN,
104
- value: tokens.refreshToken,
105
- secret: true,
106
- });
107
- }
108
- if (tokens.expiresAt) {
109
- await storage.createVariable({
110
- key: VAR_TOKEN_EXPIRES_AT,
111
- value: String(tokens.expiresAt),
112
- secret: false,
113
- });
114
- }
115
- }
116
-
117
- export async function clearTokens(storage: Storage): Promise<void> {
118
- for (const key of [VAR_ACCESS_TOKEN, VAR_REFRESH_TOKEN, VAR_TOKEN_EXPIRES_AT]) {
119
- await storage.deleteVariable({ key }).catch(() => { });
120
- }
121
- }
122
-
123
- export function formatMissingCredentials(
124
- missing: Array<"accessToken" | "openaiApiKey">,
125
- ): string {
126
- const lines = [
127
- "Linear agent setup is incomplete. Configure the following in plugin config or environment variables:",
128
- ];
129
-
130
- if (missing.includes("accessToken")) {
131
- lines.push(
132
- "- Connect Linear with `linear_connect`, or set `apiKey` / `LINEAR_API_KEY`",
133
- );
134
- }
135
- if (missing.includes("openaiApiKey")) {
136
- lines.push(
137
- "- `openaiApiKey` / `OPENAI_API_KEY` — OpenAI API key for the agent loop",
138
- );
139
- }
140
-
141
- return lines.join("\n");
142
- }
143
-
144
- export async function resolveLinearCredentials(
145
- config: LinearPluginConfig,
146
- storage: Storage,
147
- options?: { requireOpenAi?: boolean },
148
- ): Promise<
149
- | { ok: true; credentials: LinearCredentials }
150
- | { ok: false; missing: Array<"accessToken" | "openaiApiKey"> }
151
- > {
152
- const requireOpenAi = options?.requireOpenAi ?? true;
153
- const variables = (await storage.getVariables().catch(() => ({}))) as Record<
154
- string,
155
- VariableValue
156
- >;
157
-
158
- const resolve = (configKey: keyof LinearPluginConfig, envKey: string) => {
159
- const fromConfig = config[configKey];
160
- if (typeof fromConfig === "string" && fromConfig.trim()) {
161
- return fromConfig.trim();
162
- }
163
- if (process.env[envKey]?.trim()) return process.env[envKey]!.trim();
164
- return variableValue(variables, envKey)?.trim();
165
- };
166
-
167
- const openaiApiKey = resolve("openaiApiKey", "OPENAI_API_KEY");
168
- const model = resolve("model", "OPENAI_MODEL") ?? "gpt-4o-mini";
169
-
170
- let accessToken =
171
- config.apiKey ??
172
- variableValue(variables, VAR_API_KEY) ??
173
- process.env[VAR_API_KEY] ??
174
- variableValue(variables, VAR_ACCESS_TOKEN) ??
175
- process.env[VAR_ACCESS_TOKEN];
176
-
177
- const clientId =
178
- config.clientId ??
179
- variableValue(variables, VAR_CLIENT_ID) ??
180
- process.env[VAR_CLIENT_ID];
181
- const clientSecret = config.clientSecret;
182
-
183
- if (accessToken && !config.apiKey) {
184
- const expiresAtRaw =
185
- variableValue(variables, VAR_TOKEN_EXPIRES_AT) ??
186
- process.env[VAR_TOKEN_EXPIRES_AT];
187
- const expiresAt = expiresAtRaw ? Number(expiresAtRaw) : undefined;
188
- const refreshToken =
189
- variableValue(variables, VAR_REFRESH_TOKEN) ??
190
- process.env[VAR_REFRESH_TOKEN];
191
-
192
- const needsRefresh =
193
- expiresAt !== undefined &&
194
- Number.isFinite(expiresAt) &&
195
- Date.now() > expiresAt - REFRESH_MARGIN_MS;
196
-
197
- if (needsRefresh && refreshToken && clientId) {
198
- try {
199
- const tokens = await refreshAccessToken({
200
- refreshToken,
201
- clientId,
202
- clientSecret,
203
- });
204
- await saveTokens(storage, tokens);
205
- accessToken = tokens.accessToken;
206
- } catch {
207
- // Fall through; a 401 from Linear will prompt reconnect.
208
- }
209
- }
210
- }
211
-
212
- const missing: Array<"accessToken" | "openaiApiKey"> = [];
213
- if (!accessToken) missing.push("accessToken");
214
- if (requireOpenAi && !openaiApiKey) missing.push("openaiApiKey");
215
-
216
- if (missing.length > 0) {
217
- return { ok: false, missing };
218
- }
219
-
220
- return {
221
- ok: true,
222
- credentials: {
223
- accessToken: accessToken!,
224
- openaiApiKey: openaiApiKey ?? "",
225
- model,
226
- clientId,
227
- clientSecret,
228
- },
229
- };
230
- }
package/src/index.ts DELETED
@@ -1,451 +0,0 @@
1
- import {
2
- agentOutput,
3
- definePlugin,
4
- shouldHandleInvoke,
5
- toolResult,
6
- uiWidget,
7
- webhookHttpResponse,
8
- type AgentInvokeEvent,
9
- type ConfigSchema,
10
- type PluginContext,
11
- type PluginHandlerContext,
12
- type ToolActionEvent,
13
- type ToolDefinition,
14
- type WebhookEvent,
15
- } from "@meetopenbot/plugin-sdk";
16
- import {
17
- clearTokens,
18
- formatMissingCredentials,
19
- LINEAR_OPENBOT_CLIENT_ID,
20
- readLinearConfig,
21
- resolveLinearCredentials,
22
- resolveWebhookBaseUrl,
23
- saveTokens,
24
- type LinearPluginConfig,
25
- } from "./config.js";
26
- import { runLinearAgent } from "./linear-agent.js";
27
- import {
28
- buildIssuesListWidget,
29
- isAssignedIssuesPrompt,
30
- isListIssuesPrompt,
31
- issuesListTitle,
32
- } from "./linear-issues.js";
33
- import {
34
- OAUTH_WEBHOOK_PROVIDER,
35
- buildOAuthRedirectUri,
36
- fetchViewer,
37
- handleWebhookOAuthCallback,
38
- startOAuthFlow,
39
- startWebhookOAuthFlow,
40
- type OAuthFlowHandle,
41
- } from "./oauth.js";
42
-
43
- /** Client-side action handled by openbot.one — opens `value.url` in the browser. */
44
- const OPEN_URL_ACTION_ID = "open_url";
45
-
46
- const CONNECT_TOOL: ToolDefinition = {
47
- description:
48
- "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.",
49
- inputSchema: { type: "object", properties: {} },
50
- };
51
-
52
- const DISCONNECT_TOOL: ToolDefinition = {
53
- description:
54
- "Disconnect Linear: removes the stored OAuth tokens from the workspace.",
55
- inputSchema: { type: "object", properties: {} },
56
- };
57
-
58
- const CONNECT_WAIT_MS = 120 * 1000;
59
- const CONNECT_WIDGET_ID = "linear-connect";
60
- const CONNECT_PROMPT_WIDGET_ID = "linear-connect-prompt";
61
-
62
- type ConnectReplyMode = "tool" | "agent";
63
- type ConnectEvent = { meta?: ToolActionEvent["meta"] };
64
-
65
- const toolDefinitions: Record<string, ToolDefinition> = {
66
- linear_connect: CONNECT_TOOL,
67
- linear_disconnect: DISCONNECT_TOOL,
68
- };
69
-
70
- type LinearPluginFactoryContext = PluginContext & {
71
- publicBaseUrl?: string;
72
- };
73
-
74
- function* emitConnectReply(
75
- mode: ConnectReplyMode,
76
- context: PluginContext,
77
- event: ConnectEvent,
78
- data: { output: string; error?: string; [key: string]: unknown },
79
- ) {
80
- if (mode === "tool") {
81
- yield toolResult("linear_connect", event as ToolActionEvent, data);
82
- return;
83
- }
84
-
85
- yield agentOutput({
86
- agentId: context.agentId,
87
- content: data.output,
88
- threadId: event.meta?.threadId,
89
- meta: event.meta,
90
- });
91
- }
92
-
93
- function* yieldOAuthAuthorizeWidget(
94
- agentId: string,
95
- authorizeUrl: string,
96
- modeHint: string,
97
- options: {
98
- threadId?: string;
99
- meta?: ConnectEvent["meta"];
100
- widgetId?: string;
101
- },
102
- ) {
103
- yield uiWidget({
104
- agentId,
105
- threadId: options.threadId,
106
- meta: options.meta,
107
- widget: {
108
- kind: "message",
109
- widgetId: options.widgetId ?? CONNECT_WIDGET_ID,
110
- title: "Connect Linear",
111
- body: `Open Linear in your browser to authorize OpenBot. ${modeHint}`,
112
- actions: [
113
- {
114
- id: OPEN_URL_ACTION_ID,
115
- label: "Connect Linear",
116
- variant: "primary",
117
- value: { url: authorizeUrl },
118
- },
119
- ],
120
- },
121
- });
122
- }
123
-
124
- async function createOAuthHandle(
125
- context: PluginContext,
126
- publicBaseUrl?: string,
127
- ): Promise<{ handle: OAuthFlowHandle; modeHint: string } | null> {
128
- const config = readLinearConfig(context.config ?? {});
129
- const webhookBaseUrl = resolveWebhookBaseUrl(config, publicBaseUrl);
130
-
131
- if (config.apiKey || !config.clientId) return null;
132
-
133
- const scopes = config.scopes ?? "read,write,issues:create,comments:create";
134
- const handle = webhookBaseUrl
135
- ? await startWebhookOAuthFlow({
136
- storage: context.storage,
137
- clientId: config.clientId,
138
- clientSecret: config.clientSecret,
139
- scopes,
140
- webhookBaseUrl,
141
- })
142
- : startOAuthFlow({
143
- clientId: config.clientId,
144
- clientSecret: config.clientSecret,
145
- port: config.oauthPort ?? 4137,
146
- scopes,
147
- onSuccess: (tokens) => saveTokens(context.storage, tokens),
148
- });
149
-
150
- const modeHint = webhookBaseUrl
151
- ? `After approval, Linear redirects to \`${handle.redirectUri}\`.`
152
- : "After you approve access you'll be redirected back and can return here.";
153
-
154
- return { handle, modeHint };
155
- }
156
-
157
- function notConnectedMessage(
158
- missing: Array<"accessToken" | "openaiApiKey">,
159
- ): string {
160
- const needsConnect = missing.includes("accessToken");
161
- const needsOpenAi = missing.includes("openaiApiKey");
162
-
163
- if (needsConnect && needsOpenAi) {
164
- return "Linear isn't connected yet, and the OpenAI API key is missing. Connect Linear below, then add `openaiApiKey` to the plugin config.";
165
- }
166
- if (needsConnect) {
167
- return "Linear isn't connected yet. Click **Connect Linear** below to open the authorization page.";
168
- }
169
- return formatMissingCredentials(missing);
170
- }
171
-
172
- async function* handleConnect(
173
- context: PluginContext,
174
- event: ConnectEvent,
175
- publicBaseUrl?: string,
176
- replyMode: ConnectReplyMode = "tool",
177
- ) {
178
- const config = readLinearConfig(context.config ?? {});
179
- const threadId = event.meta?.threadId;
180
- const webhookBaseUrl = resolveWebhookBaseUrl(config, publicBaseUrl);
181
-
182
- if (config.apiKey) {
183
- yield* emitConnectReply(replyMode, context, event, {
184
- output:
185
- "Linear is already configured with an API key in the plugin config — no OAuth connection needed.",
186
- });
187
- return;
188
- }
189
-
190
- const oauth = await createOAuthHandle(context, publicBaseUrl);
191
- if (!oauth) {
192
- const webhookHint = webhookBaseUrl
193
- ? ` set its callback URL to ${buildOAuthRedirectUri(webhookBaseUrl)},`
194
- : ` set its callback URL to http://localhost:${config.oauthPort}/oauth/callback (local), or configure webhookBaseUrl for ${buildOAuthRedirectUri("https://<your-host>")},`;
195
-
196
- yield* emitConnectReply(replyMode, context, event, {
197
- output: [
198
- "Linear OAuth is not configured. Ways to connect:",
199
- "1. Recommended: open the OpenBot web app → Settings → Plugins and click Connect on the Linear plugin (install-time OAuth, no setup needed).",
200
- "2. Self-hosted OAuth: create an OAuth application at https://linear.app/settings/api/applications,",
201
- webhookHint,
202
- " then put its Client ID into this plugin's `clientId` config field and run linear_connect again.",
203
- "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).",
204
- ].join("\n"),
205
- });
206
- return;
207
- }
208
-
209
- const { handle, modeHint } = oauth;
210
-
211
- yield* yieldOAuthAuthorizeWidget(
212
- context.agentId,
213
- handle.authorizeUrl,
214
- modeHint,
215
- { threadId, meta: event.meta },
216
- );
217
-
218
- const outcome = await Promise.race([
219
- handle.completion,
220
- new Promise<"pending">((resolve) => {
221
- const t = setTimeout(() => resolve("pending"), CONNECT_WAIT_MS);
222
- t.unref?.();
223
- }),
224
- ]);
225
-
226
- if (outcome === "pending") {
227
- yield* emitConnectReply(replyMode, context, event, {
228
- authorizeUrl: handle.authorizeUrl,
229
- redirectUri: handle.redirectUri,
230
- output:
231
- "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.",
232
- });
233
- return;
234
- }
235
-
236
- if (!outcome) {
237
- yield* emitConnectReply(replyMode, context, event, {
238
- error: "authorization_failed",
239
- output:
240
- "Linear authorization did not complete (denied, failed, or the callback is unavailable). Run linear_connect to try again.",
241
- });
242
- return;
243
- }
244
-
245
- let who = "";
246
- try {
247
- const { viewer, organization } = await fetchViewer(outcome.accessToken);
248
- who = ` as ${viewer.displayName ?? viewer.name} in workspace "${organization.name}"`;
249
- } catch {
250
- // Connection succeeded even if the identity lookup failed.
251
- }
252
-
253
- yield uiWidget({
254
- agentId: context.agentId,
255
- threadId,
256
- widget: {
257
- kind: "message",
258
- widgetId: CONNECT_WIDGET_ID,
259
- title: "Linear connected",
260
- body: `✅ Connected to Linear${who}.`,
261
- state: "submitted",
262
- },
263
- });
264
-
265
- yield* emitConnectReply(replyMode, context, event, {
266
- connected: true,
267
- output: `Successfully connected to Linear${who}. Tokens are stored securely and refresh automatically.`,
268
- });
269
- }
270
-
271
- const linearPluginConfigSchema = {
272
- type: "object",
273
- properties: {
274
- clientId: {
275
- type: "string",
276
- description:
277
- "Linear OAuth application Client ID. Defaults to OpenBot's managed Linear application.",
278
- default: LINEAR_OPENBOT_CLIENT_ID,
279
- },
280
- clientSecret: {
281
- type: "string",
282
- description:
283
- "Linear OAuth application Client Secret (optional — PKCE is used when omitted)",
284
- format: "password",
285
- },
286
- apiKey: {
287
- type: "string",
288
- description: "Linear personal API key (alternative to OAuth)",
289
- format: "password",
290
- },
291
- oauthPort: {
292
- type: "number",
293
- description:
294
- "Local port for the OAuth callback (must match the OAuth app callback URL)",
295
- default: 4137,
296
- },
297
- scopes: {
298
- type: "string",
299
- description: "Comma-separated OAuth scopes",
300
- default: "read,write,issues:create,comments:create",
301
- },
302
- webhookBaseUrl: {
303
- type: "string",
304
- description:
305
- "Public runtime base URL for OAuth callback via /api/webhooks/linear (e.g. https://my-host.com). Falls back to host publicBaseUrl when omitted.",
306
- format: "url",
307
- },
308
- openaiApiKey: {
309
- type: "string",
310
- description: "OpenAI API key for direct Linear agent invocations",
311
- format: "password",
312
- },
313
- model: {
314
- type: "string",
315
- description:
316
- "OpenAI model id for direct Linear agent invocations (default: gpt-4o-mini)",
317
- default: "gpt-4o-mini",
318
- },
319
- },
320
- } satisfies ConfigSchema;
321
-
322
- export default definePlugin({
323
- id: "linear",
324
- name: "Linear",
325
- description:
326
- "Linear OAuth connect flow and MCP-backed agent for issues, projects, and comments",
327
- configSchema: linearPluginConfigSchema,
328
- toolDefinitions,
329
- factory: (pluginContext) => (builder) => {
330
- const { agentId, config, storage } = pluginContext;
331
- const linearConfig = readLinearConfig(config) as LinearPluginConfig;
332
- const publicBaseUrl =
333
- (pluginContext as LinearPluginFactoryContext).publicBaseUrl ?? "";
334
-
335
- builder.on("action:webhook", async function* (event) {
336
- const webhook = event as WebhookEvent;
337
- if (webhook.data.provider !== OAUTH_WEBHOOK_PROVIDER) return;
338
-
339
- const result = await handleWebhookOAuthCallback({
340
- storage,
341
- query: webhook.data.query ?? {},
342
- onSuccess: (tokens) => saveTokens(storage, tokens),
343
- });
344
-
345
- if (result.kind === "ignore") return;
346
-
347
- yield webhookHttpResponse({
348
- status: result.status,
349
- headers: { "Content-Type": "text/html; charset=utf-8" },
350
- body: result.html,
351
- });
352
- });
353
-
354
- builder.on("action:linear_connect", async function* (event: ToolActionEvent) {
355
- yield* handleConnect(pluginContext, event, publicBaseUrl);
356
- });
357
-
358
- builder.on("action:linear_disconnect", async function* (event: ToolActionEvent) {
359
- await clearTokens(storage);
360
- yield toolResult("linear_disconnect", event, {
361
- output: "Linear disconnected — stored OAuth tokens were removed.",
362
- });
363
- });
364
-
365
- builder.on("agent:invoke", async function* (
366
- event: AgentInvokeEvent,
367
- ctx: PluginHandlerContext,
368
- ) {
369
- if (!shouldHandleInvoke(event, agentId)) return;
370
-
371
- if (event.meta?.threadId) {
372
- ctx.state.threadId = event.meta.threadId;
373
- }
374
-
375
- const threadId = event.meta?.threadId ?? ctx.state.threadId;
376
- const userMessage = (event.data.content ?? "").trim();
377
- if (!userMessage || !threadId) return;
378
-
379
- const auth = await resolveLinearCredentials(linearConfig, storage);
380
- if (!auth.ok) {
381
- yield agentOutput({
382
- agentId,
383
- content: notConnectedMessage(auth.missing),
384
- threadId,
385
- meta: event.meta,
386
- });
387
-
388
- if (auth.missing.includes("accessToken")) {
389
- const oauth = await createOAuthHandle(pluginContext, publicBaseUrl);
390
- if (oauth) {
391
- yield* yieldOAuthAuthorizeWidget(
392
- agentId,
393
- oauth.handle.authorizeUrl,
394
- oauth.modeHint,
395
- {
396
- threadId,
397
- meta: event.meta,
398
- widgetId: CONNECT_PROMPT_WIDGET_ID,
399
- },
400
- );
401
- }
402
- }
403
- return;
404
- }
405
-
406
- try {
407
- const reply = await runLinearAgent({
408
- prompt: userMessage,
409
- openaiApiKey: auth.credentials.openaiApiKey,
410
- accessToken: auth.credentials.accessToken,
411
- model: auth.credentials.model,
412
- });
413
-
414
- const issues = reply.issues;
415
- const shouldListIssues =
416
- isListIssuesPrompt(userMessage) || isAssignedIssuesPrompt(userMessage);
417
-
418
- if (shouldListIssues || issues.length > 0) {
419
- yield uiWidget({
420
- agentId,
421
- threadId,
422
- meta: event.meta,
423
- widget: buildIssuesListWidget(issues, {
424
- title: issuesListTitle(userMessage),
425
- }),
426
- });
427
- }
428
-
429
- const content =
430
- reply.toolErrors.length > 0 && !reply.usedTools
431
- ? `${reply.text}\n\nMCP note: ${reply.toolErrors.join("; ")}`
432
- : reply.text;
433
-
434
- yield agentOutput({
435
- agentId,
436
- content,
437
- threadId,
438
- meta: event.meta,
439
- });
440
- } catch (error) {
441
- const message = error instanceof Error ? error.message : String(error);
442
- yield agentOutput({
443
- agentId,
444
- content: `Linear agent error: ${message}`,
445
- threadId,
446
- meta: event.meta,
447
- });
448
- }
449
- });
450
- },
451
- });