@elizaos/plugin-linear 2.0.0-alpha.7 → 2.0.0-beta.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.
Files changed (3) hide show
  1. package/dist/index.js +1820 -1619
  2. package/dist/index.js.map +29 -21
  3. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -1,11 +1,173 @@
1
+ // src/index.ts
2
+ import { getConnectorAccountManager, logger as logger14, promoteSubactionsToActions } from "@elizaos/core";
3
+
4
+ // src/accounts.ts
5
+ var DEFAULT_LINEAR_ACCOUNT_ID = "default";
6
+ var DEFAULT_LINEAR_ACCOUNT_ROLE = "OWNER";
7
+ function nonEmptyString(value) {
8
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
9
+ }
10
+ function readSetting(runtime, key) {
11
+ return nonEmptyString(runtime.getSetting(key));
12
+ }
13
+ function normalizeLinearAccountId(value) {
14
+ return nonEmptyString(value) ?? DEFAULT_LINEAR_ACCOUNT_ID;
15
+ }
16
+ function resolveLinearAccountId(runtime, options) {
17
+ const requested = nonEmptyString(options?.accountId) ?? nonEmptyString(options?.linearAccountId);
18
+ if (requested)
19
+ return requested;
20
+ const configuredDefault = readSetting(runtime, "LINEAR_DEFAULT_ACCOUNT_ID") ?? readSetting(runtime, "LINEAR_ACCOUNT_ID");
21
+ const accounts = readLinearAccounts(runtime);
22
+ const defaultAccount = resolveLinearDefaultAccount(accounts, configuredDefault);
23
+ return defaultAccount?.accountId ?? normalizeLinearAccountId(configuredDefault);
24
+ }
25
+ function parseAccountsJson(raw) {
26
+ if (!raw)
27
+ return [];
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (Array.isArray(parsed)) {
31
+ return parsed.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
32
+ }
33
+ if (parsed && typeof parsed === "object") {
34
+ return Object.entries(parsed).filter(([, value]) => value && typeof value === "object").map(([id, value]) => ({
35
+ ...value,
36
+ accountId: value.accountId ?? id
37
+ }));
38
+ }
39
+ } catch {
40
+ return [];
41
+ }
42
+ return [];
43
+ }
44
+ function readRawField(record, keys) {
45
+ const credentials = record.credentials && typeof record.credentials === "object" ? record.credentials : {};
46
+ const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
47
+ const settings = record.settings && typeof record.settings === "object" ? record.settings : {};
48
+ for (const source of [record, credentials, metadata, settings]) {
49
+ for (const key of keys) {
50
+ const value = nonEmptyString(source[key]);
51
+ if (value)
52
+ return value;
53
+ }
54
+ }
55
+ return;
56
+ }
57
+ function accountFromRecord(record) {
58
+ const accountId = normalizeLinearAccountId(record.accountId ?? record.id ?? record.name);
59
+ const apiKey = readRawField(record, [
60
+ "LINEAR_API_KEY",
61
+ "apiKey",
62
+ "token",
63
+ "accessToken",
64
+ "access"
65
+ ]);
66
+ if (!apiKey)
67
+ return null;
68
+ return {
69
+ accountId,
70
+ role: DEFAULT_LINEAR_ACCOUNT_ROLE,
71
+ apiKey,
72
+ workspaceId: readRawField(record, ["LINEAR_WORKSPACE_ID", "workspaceId"]),
73
+ defaultTeamKey: readRawField(record, ["LINEAR_DEFAULT_TEAM_KEY", "defaultTeamKey"]),
74
+ label: nonEmptyString(record.label ?? record.displayName)
75
+ };
76
+ }
77
+ function addAccount(accounts, account) {
78
+ if (account) {
79
+ accounts.set(account.accountId, account);
80
+ }
81
+ }
82
+ function readLinearAccounts(runtime) {
83
+ const accounts = new Map;
84
+ const characterConfig = runtime.character?.settings?.linear;
85
+ const characterAccounts = characterConfig?.accounts;
86
+ if (Array.isArray(characterAccounts)) {
87
+ for (const item of characterAccounts) {
88
+ if (item && typeof item === "object") {
89
+ addAccount(accounts, accountFromRecord(item));
90
+ }
91
+ }
92
+ } else if (characterAccounts && typeof characterAccounts === "object") {
93
+ for (const [id, value] of Object.entries(characterAccounts)) {
94
+ if (value && typeof value === "object") {
95
+ addAccount(accounts, accountFromRecord({
96
+ ...value,
97
+ accountId: value.accountId ?? id
98
+ }));
99
+ }
100
+ }
101
+ }
102
+ for (const record of parseAccountsJson(readSetting(runtime, "LINEAR_ACCOUNTS"))) {
103
+ addAccount(accounts, accountFromRecord(record));
104
+ }
105
+ const apiKey = readSetting(runtime, "LINEAR_API_KEY");
106
+ if (apiKey) {
107
+ addAccount(accounts, {
108
+ accountId: normalizeLinearAccountId(readSetting(runtime, "LINEAR_ACCOUNT_ID") ?? readSetting(runtime, "LINEAR_DEFAULT_ACCOUNT_ID")),
109
+ role: DEFAULT_LINEAR_ACCOUNT_ROLE,
110
+ apiKey,
111
+ workspaceId: readSetting(runtime, "LINEAR_WORKSPACE_ID"),
112
+ defaultTeamKey: readSetting(runtime, "LINEAR_DEFAULT_TEAM_KEY")
113
+ });
114
+ }
115
+ return Array.from(accounts.values());
116
+ }
117
+ function resolveLinearAccount(accounts, accountId) {
118
+ return accounts.find((account) => account.accountId === accountId) ?? null;
119
+ }
120
+ function resolveLinearDefaultAccount(accounts, accountId) {
121
+ const normalized = normalizeLinearAccountId(accountId);
122
+ return resolveLinearAccount(accounts, normalized) ?? resolveLinearAccount(accounts, DEFAULT_LINEAR_ACCOUNT_ID) ?? accounts.find((account) => account.role === DEFAULT_LINEAR_ACCOUNT_ROLE) ?? accounts[0] ?? null;
123
+ }
124
+ function hasLinearAccountConfig(runtime, options) {
125
+ const accountId = resolveLinearAccountId(runtime, options);
126
+ return Boolean(resolveLinearAccount(readLinearAccounts(runtime), accountId));
127
+ }
128
+
129
+ // src/actions/account-options.ts
130
+ function getLinearActionOptions(options) {
131
+ const direct = options ?? {};
132
+ const parameters = direct.parameters && typeof direct.parameters === "object" ? direct.parameters : {};
133
+ return { ...direct, ...parameters };
134
+ }
135
+ function getLinearAccountId(runtime, options) {
136
+ return resolveLinearAccountId(runtime, getLinearActionOptions(options));
137
+ }
138
+ var linearAccountIdParameter = {
139
+ name: "accountId",
140
+ description: "Optional Linear account id from LINEAR_ACCOUNTS. Defaults to LINEAR_DEFAULT_ACCOUNT_ID or the legacy single API key.",
141
+ required: false,
142
+ schema: { type: "string" }
143
+ };
144
+
1
145
  // src/actions/clearActivity.ts
2
146
  import {
3
- logger
147
+ logger,
148
+ requireConfirmation
4
149
  } from "@elizaos/core";
150
+
151
+ // src/actions/validate-linear-intent.ts
152
+ async function validateLinearActionIntent(runtime, _message, _state, _spec) {
153
+ try {
154
+ return hasLinearAccountConfig(runtime);
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+
160
+ // src/actions/clearActivity.ts
161
+ var CLEAR_ACTIVITY_TIMEOUT_MS = 1e4;
5
162
  var clearActivityAction = {
6
163
  name: "CLEAR_LINEAR_ACTIVITY",
7
- description: "Clear the Linear activity log",
164
+ contexts: ["tasks", "connectors", "automation"],
165
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
166
+ roleGate: { minRole: "USER" },
167
+ description: "Clear the cached Linear activity log for the connected Linear account. Use when the user asks to reset, wipe, or refresh their Linear activity history before pulling a fresh view of recent issue and comment events.",
168
+ descriptionCompressed: "clear Linear activity log",
8
169
  similes: ["clear-linear-activity", "reset-linear-activity", "delete-linear-activity"],
170
+ parameters: [linearAccountIdParameter],
9
171
  examples: [
10
172
  [
11
173
  {
@@ -38,80 +200,45 @@ var clearActivityAction = {
38
200
  }
39
201
  ]
40
202
  ],
41
- validate: async (runtime, message, state, options) => {
42
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
43
- const __avText = __avTextRaw.toLowerCase();
44
- const __avKeywords = ["clear", "linear", "activity"];
45
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
46
- const __avRegex = /\b(?:clear|linear|activity)\b/i;
47
- const __avRegexOk = __avRegex.test(__avText);
48
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
49
- const __avExpectedSource = "";
50
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
51
- const __avOptions = options && typeof options === "object" ? options : {};
52
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
53
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
54
- return false;
55
- }
56
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
57
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
58
- const __avText2 = __avTextRaw2.toLowerCase();
59
- const __avKeywords2 = ["clear", "linear", "activity"];
60
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
61
- const __avRegex2 = /\b(?:clear|linear|activity)\b/i;
62
- const __avRegexOk2 = __avRegex2.test(__avText2);
63
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
64
- const __avExpectedSource2 = "";
65
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
66
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
67
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
68
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
69
- return false;
70
- }
71
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
72
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
73
- const __avText3 = __avTextRaw3.toLowerCase();
74
- const __avKeywords3 = ["clear", "linear", "activity"];
75
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
76
- const __avRegex3 = /\b(?:clear|linear|activity)\b/i;
77
- const __avRegexOk3 = __avRegex3.test(__avText3);
78
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
79
- const __avExpectedSource3 = "";
80
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
81
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
82
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
83
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
84
- return false;
85
- }
86
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
87
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
88
- return !!apiKey;
89
- };
90
- try {
91
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
92
- } catch {
93
- return false;
94
- }
95
- };
96
- try {
97
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
98
- } catch {
99
- return false;
100
- }
101
- };
102
- try {
103
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
104
- } catch {
105
- return false;
106
- }
107
- },
203
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
204
+ keywords: ["clear", "linear", "activity"],
205
+ regexAlternation: "clear|linear|activity"
206
+ }),
108
207
  async handler(runtime, message, _state, _options, callback) {
109
208
  try {
110
209
  const linearService = runtime.getService("linear");
111
210
  if (!linearService) {
112
211
  throw new Error("Linear service not available");
113
212
  }
114
- await linearService.clearActivityLog();
213
+ const accountId = getLinearAccountId(runtime, _options);
214
+ const decision = await requireConfirmation({
215
+ runtime,
216
+ message,
217
+ actionName: "CLEAR_LINEAR_ACTIVITY",
218
+ pendingKey: `clear_log:${accountId}`,
219
+ prompt: 'Clear the Linear activity log? Reply "yes" to confirm.',
220
+ callback
221
+ });
222
+ if (decision.status === "pending") {
223
+ return {
224
+ text: "Awaiting confirmation to clear Linear activity.",
225
+ success: true,
226
+ data: { awaitingUserInput: true }
227
+ };
228
+ }
229
+ if (decision.status === "cancelled") {
230
+ const cancelMessage = "Clear of Linear activity cancelled.";
231
+ await callback?.({ text: cancelMessage, source: message.content.source });
232
+ return {
233
+ text: cancelMessage,
234
+ success: true,
235
+ data: { cancelled: true }
236
+ };
237
+ }
238
+ await Promise.race([
239
+ linearService.clearActivityLog(accountId),
240
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Linear clear activity timeout")), CLEAR_ACTIVITY_TIMEOUT_MS))
241
+ ]);
115
242
  const successMessage = "✅ Linear activity log has been cleared.";
116
243
  await callback?.({
117
244
  text: successMessage,
@@ -142,7 +269,7 @@ import {
142
269
  ModelType
143
270
  } from "@elizaos/core";
144
271
 
145
- // src/generated/prompts/typescript/prompts.ts
272
+ // src/prompts.ts
146
273
  var createCommentTemplate = `Extract comment details from the user's request to add a comment to a Linear issue.
147
274
 
148
275
  User request: "{{userMessage}}"
@@ -154,40 +281,41 @@ The user might express this in various ways:
154
281
  - "Reply to COM2-7: Thanks for the update"
155
282
  - "Let the payment issue know that it's blocked by API changes"
156
283
 
157
- Return ONLY a JSON object:
284
+ Respond with JSON only. Use this shape:
158
285
  {
159
- "issueId": "Direct issue ID if explicitly mentioned (e.g., ENG-123)",
160
- "issueDescription": "Description/keywords of the issue if no ID provided",
286
+ "issueId": "Direct issue ID if explicitly mentioned, for example ENG-123",
287
+ "issueDescription": "Description or keywords of the issue if no ID was provided",
161
288
  "commentBody": "The actual comment content to add",
162
- "commentType": "note/reply/update/question/feedback (inferred from context)"
289
+ "commentType": "note|reply|update|question|feedback"
163
290
  }
164
291
 
165
- Extract the core message the user wants to convey as the comment body.`;
292
+ Extract the core message the user wants to convey as the comment body.
293
+ Omit unknown fields. Output only the JSON object, with no prose before or after it.`;
166
294
  var createIssueTemplate = `Given the user's request, extract the information needed to create a Linear issue.
167
295
 
168
296
  User request: "{{userMessage}}"
169
297
 
170
- Extract and return ONLY a JSON object (no markdown formatting, no code blocks) with the following structure:
298
+ Respond with JSON only. Use this shape:
171
299
  {
172
300
  "title": "Brief, clear issue title",
173
- "description": "Detailed description of the issue (optional, omit or use null if not provided)",
174
- "teamKey": "Team key if mentioned (e.g., ENG, PROD) - omit or use null if not mentioned",
175
- "priority": "Priority level if mentioned (1=urgent, 2=high, 3=normal, 4=low) - omit or use null if not mentioned",
176
- "labels": ["label1", "label2"] (if any labels are mentioned, empty array if none),
177
- "assignee": "Assignee username or email if mentioned - omit or use null if not mentioned"
301
+ "description": "Detailed description of the issue",
302
+ "teamKey": "Team key if mentioned, such as ENG or PROD",
303
+ "priority": 3,
304
+ "labels": ["label"],
305
+ "assignee": "Assignee username or email if mentioned"
178
306
  }
179
307
 
180
- Return only the JSON object, no other text.`;
308
+ Omit optional fields when they are not provided. Output only the JSON object, with no prose before or after it.`;
181
309
  var deleteIssueTemplate = `Given the user's request to delete/archive a Linear issue, extract the issue identifier.
182
310
 
183
311
  User request: "{{userMessage}}"
184
312
 
185
- Extract and return ONLY a JSON object (no markdown formatting, no code blocks) with:
313
+ Respond with JSON only:
186
314
  {
187
- "issueId": "The issue identifier (e.g., ENG-123, COM2-7)"
315
+ "issueId": "The issue identifier, such as ENG-123 or COM2-7"
188
316
  }
189
317
 
190
- Return only the JSON object, no other text.`;
318
+ Output only the JSON object, with no prose before or after it.`;
191
319
  var getActivityTemplate = `Extract activity filter criteria from the user's request.
192
320
 
193
321
  User request: "{{userMessage}}"
@@ -200,22 +328,22 @@ The user might ask for activity in various ways:
200
328
  - "Recent comment activity" → action type + recency
201
329
  - "Failed operations this week" → success filter + time range
202
330
 
203
- Return ONLY a JSON object:
331
+ Respond with JSON only. Use this shape:
204
332
  {
205
333
  "timeRange": {
206
- "period": "today/yesterday/this-week/last-week/this-month",
207
- "from": "ISO datetime if specific",
208
- "to": "ISO datetime if specific"
334
+ "period": "today|yesterday|this-week|last-week|this-month",
335
+ "from": "ISO datetime if a specific start is mentioned",
336
+ "to": "ISO datetime if a specific end is mentioned"
209
337
  },
210
- "actionTypes": ["create_issue/update_issue/delete_issue/create_comment/search_issues/etc"],
211
- "resourceTypes": ["issue/project/comment/team"],
212
- "resourceId": "Specific resource ID if mentioned (e.g., ENG-123)",
213
- "user": "User name or 'me' for current user",
214
- "successFilter": "success/failed/all",
215
- "limit": number (default 10)
338
+ "actionTypes": ["create_issue"],
339
+ "resourceTypes": ["issue"],
340
+ "resourceId": "Specific resource ID if mentioned, such as ENG-123",
341
+ "user": "User name, or me for current user",
342
+ "successFilter": "success|failed|all",
343
+ "limit": 10
216
344
  }
217
345
 
218
- Only include fields that are clearly mentioned.`;
346
+ Only include fields that are clearly mentioned. Output only the JSON object, with no prose before or after it.`;
219
347
  var getIssueTemplate = `Extract issue identification from the user's request.
220
348
 
221
349
  User request: "{{userMessage}}"
@@ -227,73 +355,25 @@ The user might reference an issue by:
227
355
  - Recency (e.g., "the latest bug", "most recent issue")
228
356
  - Team context (e.g., "newest issue in ELIZA team")
229
357
 
230
- Return ONLY a JSON object:
358
+ Respond with JSON only. Use directId when an issue ID is explicitly mentioned:
359
+ {
360
+ "directId": "Issue ID such as ENG-123"
361
+ }
362
+
363
+ When no issue ID is provided, use searchBy fields:
231
364
  {
232
- "directId": "Issue ID if explicitly mentioned (e.g., ENG-123)",
233
365
  "searchBy": {
234
366
  "title": "Keywords from issue title if mentioned",
235
- "assignee": "Name/email of assignee if mentioned",
236
- "priority": "Priority level if mentioned (urgent/high/normal/low or 1-4)",
367
+ "assignee": "Name or email of assignee if mentioned",
368
+ "priority": "urgent|high|normal|low|1|2|3|4",
237
369
  "team": "Team name or key if mentioned",
238
- "state": "Issue state if mentioned (todo/in-progress/done)",
239
- "recency": "latest/newest/recent/last if mentioned",
240
- "type": "bug/feature/task if mentioned"
370
+ "state": "Issue state if mentioned, such as todo, in-progress, or done",
371
+ "recency": "latest|newest|recent|last",
372
+ "type": "bug|feature|task"
241
373
  }
242
374
  }
243
375
 
244
- Only include fields that are clearly mentioned or implied.`;
245
- var listProjectsTemplate = `Extract project filter criteria from the user's request.
246
-
247
- User request: "{{userMessage}}"
248
-
249
- The user might ask for projects in various ways:
250
- - "Show me all projects" → list all projects
251
- - "Active projects" → filter by state (active/planned/completed)
252
- - "Projects due this quarter" → filter by target date
253
- - "Which projects is Sarah managing?" → filter by lead/owner
254
- - "Projects with high priority issues" → filter by contained issue priority
255
- - "Projects for the engineering team" → filter by team
256
- - "Completed projects" → filter by state
257
- - "Projects starting next month" → filter by start date
258
-
259
- Return ONLY a JSON object:
260
- {
261
- "teamFilter": "Team name or key if mentioned",
262
- "stateFilter": "active/planned/completed/all",
263
- "dateFilter": {
264
- "type": "due/starting",
265
- "period": "this-week/this-month/this-quarter/next-month/next-quarter",
266
- "from": "ISO date if specific",
267
- "to": "ISO date if specific"
268
- },
269
- "leadFilter": "Project lead name if mentioned",
270
- "showAll": true/false (true if user explicitly asks for "all")
271
- }
272
-
273
- Only include fields that are clearly mentioned.`;
274
- var listTeamsTemplate = `Extract team filter criteria from the user's request.
275
-
276
- User request: "{{userMessage}}"
277
-
278
- The user might ask for teams in various ways:
279
- - "Show me all teams" → list all teams
280
- - "Engineering teams" → filter by teams with engineering in name/description
281
- - "List teams I'm part of" → filter by membership
282
- - "Which teams work on the mobile app?" → filter by description/focus
283
- - "Show me the ELIZA team details" → specific team lookup
284
- - "Active teams" → teams with recent activity
285
- - "Frontend and backend teams" → multiple team types
286
-
287
- Return ONLY a JSON object:
288
- {
289
- "nameFilter": "Keywords to search in team names",
290
- "specificTeam": "Specific team name or key if looking for one team",
291
- "myTeams": true/false (true if user wants their teams),
292
- "showAll": true/false (true if user explicitly asks for "all"),
293
- "includeDetails": true/false (true if user wants detailed info)
294
- }
295
-
296
- Only include fields that are clearly mentioned.`;
376
+ Only include fields that are clearly mentioned or implied. Output only the JSON object, with no prose before or after it.`;
297
377
  var searchIssuesTemplate = `Extract search criteria from the user's request for Linear issues.
298
378
 
299
379
  User request: "{{userMessage}}"
@@ -308,53 +388,199 @@ The user might express searches in various ways:
308
388
  - "Bugs that are almost done" → label + state filter
309
389
  - "Show me the oldest open issues" → state + sort order
310
390
 
311
- Extract and return ONLY a JSON object:
391
+ Respond with JSON only. Use this shape:
312
392
  {
313
- "query": "General search text for title/description",
314
- "states": ["state names like In Progress, Done, Todo, Backlog"],
315
- "assignees": ["assignee names or emails, or 'me' for current user"],
316
- "priorities": ["urgent/high/normal/low or 1/2/3/4"],
317
- "teams": ["team names or keys"],
318
- "labels": ["label names"],
319
- "hasAssignee": true/false (true = has assignee, false = unassigned),
393
+ "query": "General search text for title or description",
394
+ "states": ["In Progress"],
395
+ "assignees": ["me"],
396
+ "priorities": ["high"],
397
+ "teams": ["ENG"],
398
+ "labels": ["bug"],
399
+ "hasAssignee": true,
320
400
  "dateRange": {
321
- "field": "created/updated/completed",
322
- "period": "today/yesterday/this-week/last-week/this-month/last-month",
323
- "from": "ISO date if specific date",
324
- "to": "ISO date if specific date"
401
+ "field": "created|updated|completed",
402
+ "period": "today|yesterday|this-week|last-week|this-month|last-month",
403
+ "from": "ISO date if a specific start is mentioned",
404
+ "to": "ISO date if a specific end is mentioned"
325
405
  },
326
406
  "sort": {
327
- "field": "created/updated/priority",
328
- "order": "asc/desc"
407
+ "field": "created|updated|priority",
408
+ "order": "asc|desc"
329
409
  },
330
- "limit": number (default 10)
410
+ "limit": 10
331
411
  }
332
412
 
333
- Only include fields that are clearly mentioned or implied. For "my" issues, set assignees to ["me"].`;
413
+ Only include fields that are clearly mentioned or implied. For "my" issues, set assignees to ["me"]. Output only the JSON object, with no prose before or after it.`;
334
414
  var updateIssueTemplate = `Given the user's request to update a Linear issue, extract the information needed.
335
415
 
336
416
  User request: "{{userMessage}}"
337
417
 
338
- Extract and return ONLY a JSON object (no markdown formatting, no code blocks) with the following structure:
418
+ Respond with JSON only. Use this shape:
339
419
  {
340
- "issueId": "The issue identifier (e.g., ENG-123, COM2-7)",
420
+ "issueId": "Issue identifier such as ENG-123 or COM2-7",
341
421
  "updates": {
342
422
  "title": "New title if changing the title",
343
423
  "description": "New description if changing the description",
344
- "priority": "Priority level if changing (1=urgent, 2=high, 3=normal, 4=low)",
345
- "teamKey": "New team key if moving to another team (e.g., ENG, ELIZA, COM2)",
424
+ "priority": 3,
425
+ "teamKey": "New team key if moving to another team, such as ENG, ELIZA, or COM2",
346
426
  "assignee": "New assignee username or email if changing",
347
- "status": "New status if changing (e.g., todo, in-progress, done, canceled)",
348
- "labels": ["label1", "label2"] (if changing labels, empty array to clear)
427
+ "status": "todo|in-progress|done|canceled",
428
+ "labels": ["label"]
349
429
  }
350
430
  }
351
431
 
352
- Only include fields that are being updated. Return only the JSON object, no other text.`;
432
+ Only include fields that are being updated. Use an empty labels array to clear all labels. Output only the JSON object, with no prose before or after it.`;
433
+
434
+ // src/actions/parseLinearPrompt.ts
435
+ var EMPTY_SCALAR_VALUES = new Set(["", "none", "null", "undefined", "n/a", "not provided"]);
436
+ var EMPTY_LIST_VALUES = new Set([...EMPTY_SCALAR_VALUES, "clear", "clear all", "no labels"]);
437
+ function isRecord(value) {
438
+ return typeof value === "object" && value !== null && !Array.isArray(value);
439
+ }
440
+ function stripWrappingQuotes(value) {
441
+ const trimmed = value.trim();
442
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
443
+ return trimmed.slice(1, -1).trim();
444
+ }
445
+ return trimmed;
446
+ }
447
+ function normalizeListEntry(value) {
448
+ return stripWrappingQuotes(value.replace(/^\s*[-*]\s*/, "").trim());
449
+ }
450
+ function splitListString(value) {
451
+ let trimmed = value.trim();
452
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
453
+ trimmed = trimmed.slice(1, -1);
454
+ }
455
+ return trimmed.split(/[,\n]/).map(normalizeListEntry).filter(Boolean);
456
+ }
457
+ function parseLinearPromptResponse(response) {
458
+ try {
459
+ const trimmed = response.trim();
460
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
461
+ const candidate = (fenced?.[1] ?? trimmed).trim();
462
+ const firstBrace = candidate.indexOf("{");
463
+ const lastBrace = candidate.lastIndexOf("}");
464
+ if (firstBrace < 0 || lastBrace <= firstBrace)
465
+ return {};
466
+ const parsed = JSON.parse(candidate.slice(firstBrace, lastBrace + 1));
467
+ return isRecord(parsed) ? parsed : {};
468
+ } catch {
469
+ return {};
470
+ }
471
+ }
472
+ function getRecordValue(value) {
473
+ if (isRecord(value)) {
474
+ return value;
475
+ }
476
+ if (typeof value === "string" && value.trim().startsWith("{")) {
477
+ const parsed = parseLinearPromptResponse(value);
478
+ return Object.keys(parsed).length > 0 ? parsed : undefined;
479
+ }
480
+ return;
481
+ }
482
+ function getStringValue(value) {
483
+ if (typeof value === "string") {
484
+ const normalized = stripWrappingQuotes(value);
485
+ return EMPTY_SCALAR_VALUES.has(normalized.toLowerCase()) ? undefined : normalized;
486
+ }
487
+ if (typeof value === "number" || typeof value === "boolean") {
488
+ return String(value);
489
+ }
490
+ return;
491
+ }
492
+ function getStringArrayValue(value) {
493
+ if (Array.isArray(value)) {
494
+ return value.flatMap((entry) => {
495
+ if (entry == null) {
496
+ return [];
497
+ }
498
+ if (typeof entry === "string") {
499
+ return splitListString(entry);
500
+ }
501
+ return [String(entry)];
502
+ }).filter(Boolean);
503
+ }
504
+ if (typeof value === "string") {
505
+ const normalized = stripWrappingQuotes(value);
506
+ if (EMPTY_LIST_VALUES.has(normalized.toLowerCase())) {
507
+ return [];
508
+ }
509
+ return splitListString(normalized);
510
+ }
511
+ return;
512
+ }
513
+ function getBooleanValue(value) {
514
+ if (typeof value === "boolean") {
515
+ return value;
516
+ }
517
+ if (typeof value === "string") {
518
+ const normalized = value.trim().toLowerCase();
519
+ if (["true", "yes", "y"].includes(normalized)) {
520
+ return true;
521
+ }
522
+ if (["false", "no", "n"].includes(normalized)) {
523
+ return false;
524
+ }
525
+ }
526
+ return;
527
+ }
528
+ function getNumberValue(value) {
529
+ if (typeof value === "number" && Number.isFinite(value)) {
530
+ return value;
531
+ }
532
+ if (typeof value === "string") {
533
+ const normalized = value.trim();
534
+ if (!normalized) {
535
+ return;
536
+ }
537
+ const parsed = Number(normalized);
538
+ return Number.isFinite(parsed) ? parsed : undefined;
539
+ }
540
+ return;
541
+ }
542
+ function getPriorityNumberValue(value) {
543
+ const numeric = getNumberValue(value);
544
+ if (numeric) {
545
+ return numeric;
546
+ }
547
+ const priority = getStringValue(value)?.toLowerCase();
548
+ if (!priority) {
549
+ return;
550
+ }
551
+ const priorityMap = {
552
+ urgent: 1,
553
+ high: 2,
554
+ normal: 3,
555
+ medium: 3,
556
+ low: 4
557
+ };
558
+ return priorityMap[priority];
559
+ }
353
560
 
354
561
  // src/actions/createComment.ts
355
562
  var createCommentAction = {
356
563
  name: "CREATE_LINEAR_COMMENT",
564
+ contexts: ["tasks", "connectors", "automation"],
565
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
566
+ roleGate: { minRole: "USER" },
357
567
  description: "Add a comment to a Linear issue",
568
+ descriptionCompressed: "add comment Linear issue",
569
+ parameters: [
570
+ {
571
+ name: "issueId",
572
+ description: "Linear issue id or identifier to comment on.",
573
+ required: false,
574
+ schema: { type: "string" }
575
+ },
576
+ {
577
+ name: "body",
578
+ description: "Comment body to add to the issue.",
579
+ required: false,
580
+ schema: { type: "string" }
581
+ },
582
+ linearAccountIdParameter
583
+ ],
358
584
  similes: [
359
585
  "create-linear-comment",
360
586
  "add-linear-comment",
@@ -408,79 +634,17 @@ var createCommentAction = {
408
634
  }
409
635
  ]
410
636
  ],
411
- validate: async (runtime, message, state, options) => {
412
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
413
- const __avText = __avTextRaw.toLowerCase();
414
- const __avKeywords = ["create", "linear", "comment"];
415
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
416
- const __avRegex = /\b(?:create|linear|comment)\b/i;
417
- const __avRegexOk = __avRegex.test(__avText);
418
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
419
- const __avExpectedSource = "";
420
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
421
- const __avOptions = options && typeof options === "object" ? options : {};
422
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
423
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
424
- return false;
425
- }
426
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
427
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
428
- const __avText2 = __avTextRaw2.toLowerCase();
429
- const __avKeywords2 = ["create", "linear", "comment"];
430
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
431
- const __avRegex2 = /\b(?:create|linear|comment)\b/i;
432
- const __avRegexOk2 = __avRegex2.test(__avText2);
433
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
434
- const __avExpectedSource2 = "";
435
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
436
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
437
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
438
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
439
- return false;
440
- }
441
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
442
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
443
- const __avText3 = __avTextRaw3.toLowerCase();
444
- const __avKeywords3 = ["create", "linear", "comment"];
445
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
446
- const __avRegex3 = /\b(?:create|linear|comment)\b/i;
447
- const __avRegexOk3 = __avRegex3.test(__avText3);
448
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
449
- const __avExpectedSource3 = "";
450
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
451
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
452
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
453
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
454
- return false;
455
- }
456
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
457
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
458
- return !!apiKey;
459
- };
460
- try {
461
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
462
- } catch {
463
- return false;
464
- }
465
- };
466
- try {
467
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
468
- } catch {
469
- return false;
470
- }
471
- };
472
- try {
473
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
474
- } catch {
475
- return false;
476
- }
477
- },
637
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
638
+ keywords: ["create", "linear", "comment"],
639
+ regexAlternation: "create|linear|comment"
640
+ }),
478
641
  async handler(runtime, message, _state, _options, callback) {
479
642
  try {
480
643
  const linearService = runtime.getService("linear");
481
644
  if (!linearService) {
482
645
  throw new Error("Linear service not available");
483
646
  }
647
+ const accountId = getLinearAccountId(runtime, _options);
484
648
  const content = message.content.text;
485
649
  if (!content) {
486
650
  const errorMessage = "Please provide a message with the issue and comment content.";
@@ -514,22 +678,28 @@ var createCommentAction = {
514
678
  }
515
679
  } else {
516
680
  try {
517
- const parsed = JSON.parse(response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
518
- if (parsed.issueId) {
519
- issueId = parsed.issueId;
520
- commentBody = parsed.commentBody;
521
- } else if (parsed.issueDescription) {
681
+ const parsed = parseLinearPromptResponse(response);
682
+ if (Object.keys(parsed).length === 0) {
683
+ throw new Error("No fields found in model response");
684
+ }
685
+ const parsedIssueId = getStringValue(parsed.issueId);
686
+ const issueDescription = getStringValue(parsed.issueDescription);
687
+ const parsedCommentBody = getStringValue(parsed.commentBody) ?? "";
688
+ if (parsedIssueId) {
689
+ issueId = parsedIssueId;
690
+ commentBody = parsedCommentBody;
691
+ } else if (issueDescription) {
522
692
  const filters = {
523
- query: parsed.issueDescription,
693
+ query: issueDescription,
524
694
  limit: 5
525
695
  };
526
- const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
696
+ const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
527
697
  if (defaultTeamKey) {
528
698
  filters.team = defaultTeamKey;
529
699
  }
530
- const issues = await linearService.searchIssues(filters);
700
+ const issues = await linearService.searchIssues(filters, accountId);
531
701
  if (issues.length === 0) {
532
- const errorMessage = `No issues found matching "${parsed.issueDescription}". Please provide a specific issue ID.`;
702
+ const errorMessage = `No issues found matching "${issueDescription}". Please provide a specific issue ID.`;
533
703
  await callback?.({
534
704
  text: errorMessage,
535
705
  source: message.content.source
@@ -541,13 +711,13 @@ var createCommentAction = {
541
711
  }
542
712
  if (issues.length === 1) {
543
713
  issueId = issues[0].identifier;
544
- commentBody = parsed.commentBody;
714
+ commentBody = parsedCommentBody;
545
715
  } else {
546
716
  const issueList = await Promise.all(issues.map(async (issue2, index) => {
547
717
  const state = await issue2.state;
548
718
  return `${index + 1}. ${issue2.identifier}: ${issue2.title} (${state?.name || "No state"})`;
549
719
  }));
550
- const clarifyMessage = `Found multiple issues matching "${parsed.issueDescription}":
720
+ const clarifyMessage = `Found multiple issues matching "${issueDescription}":
551
721
  ${issueList.join(`
552
722
  `)}
553
723
 
@@ -566,15 +736,16 @@ Please specify which issue to comment on by its ID.`;
566
736
  identifier: i.identifier,
567
737
  title: i.title
568
738
  })),
569
- pendingComment: parsed.commentBody
739
+ pendingComment: parsedCommentBody
570
740
  }
571
741
  };
572
742
  }
573
743
  } else {
574
744
  throw new Error("No issue identifier or description found");
575
745
  }
576
- if (parsed.commentType && parsed.commentType !== "note") {
577
- commentBody = `[${parsed.commentType.toUpperCase()}] ${commentBody}`;
746
+ const commentType = getStringValue(parsed.commentType)?.toLowerCase();
747
+ if (commentType && commentType !== "note") {
748
+ commentBody = `[${commentType.toUpperCase()}] ${commentBody}`;
578
749
  }
579
750
  } catch (parseError) {
580
751
  logger2.warn("Failed to parse LLM response, falling back to regex:", parseError);
@@ -606,11 +777,11 @@ Please specify which issue to comment on by its ID.`;
606
777
  success: false
607
778
  };
608
779
  }
609
- const issue = await linearService.getIssue(issueId);
780
+ const issue = await linearService.getIssue(issueId, accountId);
610
781
  const comment = await linearService.createComment({
611
782
  issueId: issue.id,
612
783
  body: commentBody
613
- });
784
+ }, accountId);
614
785
  const successMessage = `✅ Comment added to issue ${issue.identifier}: "${commentBody}"`;
615
786
  await callback?.({
616
787
  text: successMessage,
@@ -624,7 +795,8 @@ Please specify which issue to comment on by its ID.`;
624
795
  issueId: issue.id,
625
796
  issueIdentifier: issue.identifier,
626
797
  commentBody,
627
- createdAt: comment.createdAt instanceof Date ? comment.createdAt.toISOString() : comment.createdAt
798
+ createdAt: comment.createdAt instanceof Date ? comment.createdAt.toISOString() : comment.createdAt,
799
+ accountId
628
800
  }
629
801
  };
630
802
  } catch (error) {
@@ -649,7 +821,30 @@ import {
649
821
  } from "@elizaos/core";
650
822
  var createIssueAction = {
651
823
  name: "CREATE_LINEAR_ISSUE",
652
- description: "Create a new issue in Linear",
824
+ contexts: ["tasks", "connectors", "automation"],
825
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
826
+ roleGate: { minRole: "USER" },
827
+ description: "Create a new Linear issue with title, description, priority, team, assignee, and labels. Use when the user wants to file, log, or track a new ticket, bug, story, or task in Linear from chat.",
828
+ descriptionCompressed: "create new issue Linear",
829
+ parameters: [
830
+ {
831
+ name: "issueData",
832
+ description: "Structured Linear issue fields.",
833
+ required: false,
834
+ schema: {
835
+ type: "object",
836
+ properties: {
837
+ title: { type: "string" },
838
+ description: { type: "string" },
839
+ priority: { type: "number" },
840
+ teamId: { type: "string" },
841
+ assigneeId: { type: "string" },
842
+ labelIds: { type: "array", items: { type: "string" } }
843
+ }
844
+ }
845
+ },
846
+ linearAccountIdParameter
847
+ ],
653
848
  similes: ["create-linear-issue", "new-linear-issue", "add-linear-issue"],
654
849
  examples: [
655
850
  [
@@ -683,79 +878,17 @@ var createIssueAction = {
683
878
  }
684
879
  ]
685
880
  ],
686
- validate: async (runtime, message, state, options) => {
687
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
688
- const __avText = __avTextRaw.toLowerCase();
689
- const __avKeywords = ["create", "linear", "issue"];
690
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
691
- const __avRegex = /\b(?:create|linear|issue)\b/i;
692
- const __avRegexOk = __avRegex.test(__avText);
693
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
694
- const __avExpectedSource = "";
695
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
696
- const __avOptions = options && typeof options === "object" ? options : {};
697
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
698
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
699
- return false;
700
- }
701
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
702
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
703
- const __avText2 = __avTextRaw2.toLowerCase();
704
- const __avKeywords2 = ["create", "linear", "issue"];
705
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
706
- const __avRegex2 = /\b(?:create|linear|issue)\b/i;
707
- const __avRegexOk2 = __avRegex2.test(__avText2);
708
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
709
- const __avExpectedSource2 = "";
710
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
711
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
712
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
713
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
714
- return false;
715
- }
716
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
717
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
718
- const __avText3 = __avTextRaw3.toLowerCase();
719
- const __avKeywords3 = ["create", "linear", "issue"];
720
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
721
- const __avRegex3 = /\b(?:create|linear|issue)\b/i;
722
- const __avRegexOk3 = __avRegex3.test(__avText3);
723
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
724
- const __avExpectedSource3 = "";
725
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
726
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
727
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
728
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
729
- return false;
730
- }
731
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
732
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
733
- return !!apiKey;
734
- };
735
- try {
736
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
737
- } catch {
738
- return false;
739
- }
740
- };
741
- try {
742
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
743
- } catch {
744
- return false;
745
- }
746
- };
747
- try {
748
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
749
- } catch {
750
- return false;
751
- }
752
- },
881
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
882
+ keywords: ["create", "linear", "issue"],
883
+ regexAlternation: "create|linear|issue"
884
+ }),
753
885
  async handler(runtime, message, _state, _options, callback) {
754
886
  try {
755
887
  const linearService = runtime.getService("linear");
756
888
  if (!linearService) {
757
889
  throw new Error("Linear service not available");
758
890
  }
891
+ const accountId = getLinearAccountId(runtime, _options);
759
892
  const content = message.content.text;
760
893
  if (!content) {
761
894
  const errorMessage = "Please provide a description for the issue.";
@@ -782,37 +915,40 @@ var createIssueAction = {
782
915
  throw new Error("Failed to extract issue information");
783
916
  }
784
917
  try {
785
- const cleanedResponse = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
786
- const parsed = JSON.parse(cleanedResponse);
918
+ const parsed = parseLinearPromptResponse(response);
919
+ if (Object.keys(parsed).length === 0) {
920
+ throw new Error("No fields found in model response");
921
+ }
787
922
  issueData = {
788
- title: parsed.title || undefined,
789
- description: parsed.description || undefined,
790
- priority: parsed.priority ? Number(parsed.priority) : undefined
923
+ title: getStringValue(parsed.title),
924
+ description: getStringValue(parsed.description),
925
+ priority: getPriorityNumberValue(parsed.priority)
791
926
  };
792
- if (parsed.teamKey) {
793
- const teams = await linearService.getTeams();
794
- const team = teams.find((t) => t.key.toLowerCase() === parsed.teamKey.toLowerCase());
927
+ const teamKey = getStringValue(parsed.teamKey);
928
+ if (teamKey) {
929
+ const teams = await linearService.getTeams(accountId);
930
+ const team = teams.find((t) => t.key.toLowerCase() === teamKey.toLowerCase());
795
931
  if (team) {
796
932
  issueData.teamId = team.id;
797
933
  }
798
934
  }
799
- if (parsed.assignee && parsed.assignee !== "") {
800
- const cleanAssignee = parsed.assignee.replace(/^@/, "");
801
- const users = await linearService.getUsers();
935
+ const assignee = getStringValue(parsed.assignee);
936
+ if (assignee) {
937
+ const cleanAssignee = assignee.replace(/^@/, "");
938
+ const users = await linearService.getUsers(accountId);
802
939
  const user = users.find((u) => u.email === cleanAssignee || u.name.toLowerCase().includes(cleanAssignee.toLowerCase()));
803
940
  if (user) {
804
941
  issueData.assigneeId = user.id;
805
942
  }
806
943
  }
807
- if (parsed.labels && Array.isArray(parsed.labels) && parsed.labels.length > 0) {
808
- const labels = await linearService.getLabels(issueData.teamId);
944
+ const parsedLabels = getStringArrayValue(parsed.labels);
945
+ if (parsedLabels && parsedLabels.length > 0) {
946
+ const labels = await linearService.getLabels(issueData.teamId, accountId);
809
947
  const labelIds = [];
810
- for (const labelName of parsed.labels) {
811
- if (labelName && labelName !== "") {
812
- const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
813
- if (label) {
814
- labelIds.push(label.id);
815
- }
948
+ for (const labelName of parsedLabels) {
949
+ const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
950
+ if (label) {
951
+ labelIds.push(label.id);
816
952
  }
817
953
  }
818
954
  if (labelIds.length > 0) {
@@ -820,9 +956,9 @@ var createIssueAction = {
820
956
  }
821
957
  }
822
958
  if (!issueData.teamId) {
823
- const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
959
+ const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
824
960
  if (defaultTeamKey) {
825
- const teams = await linearService.getTeams();
961
+ const teams = await linearService.getTeams(accountId);
826
962
  const defaultTeam = teams.find((t) => t.key.toLowerCase() === defaultTeamKey.toLowerCase());
827
963
  if (defaultTeam) {
828
964
  issueData.teamId = defaultTeam.id;
@@ -832,7 +968,7 @@ var createIssueAction = {
832
968
  }
833
969
  }
834
970
  if (!issueData.teamId) {
835
- const teams = await linearService.getTeams();
971
+ const teams = await linearService.getTeams(accountId);
836
972
  if (teams.length > 0) {
837
973
  issueData.teamId = teams[0].id;
838
974
  logger3.warn(`No team specified, using first available team: ${teams[0].name}`);
@@ -845,8 +981,8 @@ var createIssueAction = {
845
981
  title: content.length > 100 ? `${content.substring(0, 100)}...` : content,
846
982
  description: content
847
983
  };
848
- const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
849
- const teams = await linearService.getTeams();
984
+ const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
985
+ const teams = await linearService.getTeams(accountId);
850
986
  if (defaultTeamKey) {
851
987
  const defaultTeam = teams.find((t) => t.key.toLowerCase() === defaultTeamKey.toLowerCase());
852
988
  if (defaultTeam) {
@@ -882,7 +1018,7 @@ var createIssueAction = {
882
1018
  success: false
883
1019
  };
884
1020
  }
885
- const issue = await linearService.createIssue(issueData);
1021
+ const issue = await linearService.createIssue(issueData, accountId);
886
1022
  const successMessage = `✅ Created Linear issue: ${issue.title} (${issue.identifier})
887
1023
 
888
1024
  View it at: ${issue.url}`;
@@ -896,7 +1032,8 @@ View it at: ${issue.url}`;
896
1032
  data: {
897
1033
  issueId: issue.id,
898
1034
  identifier: issue.identifier,
899
- url: issue.url
1035
+ url: issue.url,
1036
+ accountId
900
1037
  }
901
1038
  };
902
1039
  } catch (error) {
@@ -914,14 +1051,101 @@ View it at: ${issue.url}`;
914
1051
  }
915
1052
  };
916
1053
 
1054
+ // src/actions/deleteComment.ts
1055
+ import {
1056
+ logger as logger4
1057
+ } from "@elizaos/core";
1058
+ var deleteCommentAction = {
1059
+ name: "DELETE_LINEAR_COMMENT",
1060
+ contexts: ["tasks", "connectors", "automation"],
1061
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
1062
+ roleGate: { minRole: "USER" },
1063
+ description: "Delete a specific Linear comment by its comment id. Use when the user asks to remove, retract, or erase a comment they previously left on a Linear issue.",
1064
+ descriptionCompressed: "delete Linear comment id",
1065
+ parameters: [
1066
+ {
1067
+ name: "commentId",
1068
+ description: "Linear comment id to delete.",
1069
+ required: false,
1070
+ schema: { type: "string" }
1071
+ },
1072
+ linearAccountIdParameter
1073
+ ],
1074
+ similes: ["remove-linear-comment", "erase-linear-comment"],
1075
+ examples: [
1076
+ [
1077
+ {
1078
+ name: "User",
1079
+ content: { text: "Delete comment abc-123 from ENG-456." }
1080
+ },
1081
+ {
1082
+ name: "Assistant",
1083
+ content: {
1084
+ text: "I'll delete that comment.",
1085
+ actions: ["DELETE_LINEAR_COMMENT"]
1086
+ }
1087
+ }
1088
+ ]
1089
+ ],
1090
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
1091
+ keywords: ["delete", "remove", "linear", "comment"],
1092
+ regexAlternation: "delete|remove|linear|comment"
1093
+ }),
1094
+ async handler(runtime, message, _state, _options, callback) {
1095
+ try {
1096
+ const linearService = runtime.getService("linear");
1097
+ if (!linearService) {
1098
+ throw new Error("Linear service not available");
1099
+ }
1100
+ const accountId = getLinearAccountId(runtime, _options);
1101
+ const params = _options?.parameters;
1102
+ const commentId = params?.commentId?.trim() ?? "";
1103
+ if (!commentId) {
1104
+ const errorMessage = "Please provide a commentId to delete.";
1105
+ await callback?.({ text: errorMessage, source: message.content.source });
1106
+ return { text: errorMessage, success: false };
1107
+ }
1108
+ await linearService.deleteComment(commentId, accountId);
1109
+ const successMessage = `Deleted comment ${commentId}.`;
1110
+ await callback?.({ text: successMessage, source: message.content.source });
1111
+ return {
1112
+ text: successMessage,
1113
+ success: true,
1114
+ data: { commentId, accountId }
1115
+ };
1116
+ } catch (error) {
1117
+ logger4.error("Failed to delete comment:", error);
1118
+ const errorMessage = `Failed to delete comment: ${error instanceof Error ? error.message : "Unknown error"}`;
1119
+ await callback?.({ text: errorMessage, source: message.content.source });
1120
+ return { text: errorMessage, success: false };
1121
+ }
1122
+ }
1123
+ };
1124
+
917
1125
  // src/actions/deleteIssue.ts
918
1126
  import {
919
- logger as logger4,
920
- ModelType as ModelType3
1127
+ logger as logger5,
1128
+ ModelType as ModelType3,
1129
+ requireConfirmation as requireConfirmation2
921
1130
  } from "@elizaos/core";
1131
+ var LINEAR_MODEL_TIMEOUT_MS = 15000;
1132
+ var LINEAR_ISSUE_TITLE_MAX_CHARS = 300;
922
1133
  var deleteIssueAction = {
923
1134
  name: "DELETE_LINEAR_ISSUE",
1135
+ contexts: ["tasks", "connectors", "automation"],
1136
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
1137
+ roleGate: { minRole: "USER" },
924
1138
  description: "Delete (archive) an issue in Linear",
1139
+ descriptionCompressed: "delete (archive) issue Linear",
1140
+ parameters: [
1141
+ {
1142
+ name: "issueId",
1143
+ description: "Linear issue id or identifier to archive.",
1144
+ required: false,
1145
+ schema: { type: "string" }
1146
+ },
1147
+ linearAccountIdParameter
1148
+ ],
925
1149
  similes: [
926
1150
  "delete-linear-issue",
927
1151
  "archive-linear-issue",
@@ -975,79 +1199,17 @@ var deleteIssueAction = {
975
1199
  }
976
1200
  ]
977
1201
  ],
978
- validate: async (runtime, message, state, options) => {
979
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
980
- const __avText = __avTextRaw.toLowerCase();
981
- const __avKeywords = ["delete", "linear", "issue"];
982
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
983
- const __avRegex = /\b(?:delete|linear|issue)\b/i;
984
- const __avRegexOk = __avRegex.test(__avText);
985
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
986
- const __avExpectedSource = "";
987
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
988
- const __avOptions = options && typeof options === "object" ? options : {};
989
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
990
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
991
- return false;
992
- }
993
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
994
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
995
- const __avText2 = __avTextRaw2.toLowerCase();
996
- const __avKeywords2 = ["delete", "linear", "issue"];
997
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
998
- const __avRegex2 = /\b(?:delete|linear|issue)\b/i;
999
- const __avRegexOk2 = __avRegex2.test(__avText2);
1000
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
1001
- const __avExpectedSource2 = "";
1002
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
1003
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
1004
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
1005
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
1006
- return false;
1007
- }
1008
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
1009
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
1010
- const __avText3 = __avTextRaw3.toLowerCase();
1011
- const __avKeywords3 = ["delete", "linear", "issue"];
1012
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
1013
- const __avRegex3 = /\b(?:delete|linear|issue)\b/i;
1014
- const __avRegexOk3 = __avRegex3.test(__avText3);
1015
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
1016
- const __avExpectedSource3 = "";
1017
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
1018
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
1019
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
1020
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
1021
- return false;
1022
- }
1023
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
1024
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
1025
- return !!apiKey;
1026
- };
1027
- try {
1028
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
1029
- } catch {
1030
- return false;
1031
- }
1032
- };
1033
- try {
1034
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
1035
- } catch {
1036
- return false;
1037
- }
1038
- };
1039
- try {
1040
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
1041
- } catch {
1042
- return false;
1043
- }
1044
- },
1202
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
1203
+ keywords: ["delete", "linear", "issue"],
1204
+ regexAlternation: "delete|linear|issue"
1205
+ }),
1045
1206
  async handler(runtime, message, _state, _options, callback) {
1046
1207
  try {
1047
1208
  const linearService = runtime.getService("linear");
1048
1209
  if (!linearService) {
1049
1210
  throw new Error("Linear service not available");
1050
1211
  }
1212
+ const accountId = getLinearAccountId(runtime, _options);
1051
1213
  const content = message.content.text;
1052
1214
  if (!content) {
1053
1215
  const errorMessage = "Please specify which issue to delete.";
@@ -1066,21 +1228,26 @@ var deleteIssueAction = {
1066
1228
  issueId = params.issueId;
1067
1229
  } else {
1068
1230
  const prompt = deleteIssueTemplate.replace("{{userMessage}}", content);
1069
- const response = await runtime.useModel(ModelType3.TEXT_LARGE, {
1070
- prompt
1071
- });
1231
+ const response = await Promise.race([
1232
+ runtime.useModel(ModelType3.TEXT_LARGE, {
1233
+ prompt
1234
+ }),
1235
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Linear issue extraction timeout")), LINEAR_MODEL_TIMEOUT_MS))
1236
+ ]);
1072
1237
  if (!response) {
1073
1238
  throw new Error("Failed to extract issue identifier");
1074
1239
  }
1075
1240
  try {
1076
- const cleanedResponse = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
1077
- const parsed = JSON.parse(cleanedResponse);
1078
- issueId = parsed.issueId;
1241
+ const parsed = parseLinearPromptResponse(response);
1242
+ if (Object.keys(parsed).length === 0) {
1243
+ throw new Error("No fields found in model response");
1244
+ }
1245
+ issueId = getStringValue(parsed.issueId) ?? "";
1079
1246
  if (!issueId) {
1080
1247
  throw new Error("Issue ID not found in parsed response");
1081
1248
  }
1082
1249
  } catch (parseError) {
1083
- logger4.warn("Failed to parse LLM response, falling back to regex parsing:", parseError);
1250
+ logger5.warn("Failed to parse LLM response, falling back to regex parsing:", parseError);
1084
1251
  const issueMatch = content.match(/(\w+-\d+)/);
1085
1252
  if (!issueMatch) {
1086
1253
  const errorMessage = "Please specify an issue ID (e.g., ENG-123) to delete.";
@@ -1096,11 +1263,35 @@ var deleteIssueAction = {
1096
1263
  issueId = issueMatch[1];
1097
1264
  }
1098
1265
  }
1099
- const issue = await linearService.getIssue(issueId);
1100
- const issueTitle = issue.title;
1266
+ const issue = await linearService.getIssue(issueId, accountId);
1267
+ const issueTitle = issue.title.slice(0, LINEAR_ISSUE_TITLE_MAX_CHARS);
1101
1268
  const issueIdentifier = issue.identifier;
1102
- logger4.info(`Archiving issue ${issueIdentifier}: ${issueTitle}`);
1103
- await linearService.deleteIssue(issueId);
1269
+ const decision = await requireConfirmation2({
1270
+ runtime,
1271
+ message,
1272
+ actionName: "DELETE_LINEAR_ISSUE",
1273
+ pendingKey: `archive:${issue.id}`,
1274
+ prompt: `Archive issue ${issueIdentifier}: "${issueTitle}"? This moves it out of active views. Reply "yes" to confirm.`,
1275
+ callback
1276
+ });
1277
+ if (decision.status === "pending") {
1278
+ return {
1279
+ text: `Awaiting confirmation to archive ${issueIdentifier}.`,
1280
+ success: true,
1281
+ data: { awaitingUserInput: true, issueId: issue.id, identifier: issueIdentifier }
1282
+ };
1283
+ }
1284
+ if (decision.status === "cancelled") {
1285
+ const cancelMessage = `Archive of ${issueIdentifier} cancelled.`;
1286
+ await callback?.({ text: cancelMessage, source: message.content.source });
1287
+ return {
1288
+ text: cancelMessage,
1289
+ success: true,
1290
+ data: { cancelled: true, issueId: issue.id, identifier: issueIdentifier }
1291
+ };
1292
+ }
1293
+ logger5.info(`Archiving issue ${issueIdentifier}: ${issueTitle}`);
1294
+ await linearService.deleteIssue(issueId, accountId);
1104
1295
  const successMessage = `✅ Successfully archived issue ${issueIdentifier}: "${issueTitle}"
1105
1296
 
1106
1297
  The issue has been moved to the archived state and will no longer appear in active views.`;
@@ -1115,11 +1306,12 @@ The issue has been moved to the archived state and will no longer appear in acti
1115
1306
  issueId: issue.id,
1116
1307
  identifier: issueIdentifier,
1117
1308
  title: issueTitle,
1118
- archived: true
1309
+ archived: true,
1310
+ accountId
1119
1311
  }
1120
1312
  };
1121
1313
  } catch (error) {
1122
- logger4.error("Failed to delete issue:", error);
1314
+ logger5.error("Failed to delete issue:", error);
1123
1315
  const errorMessage = `❌ Failed to delete issue: ${error instanceof Error ? error.message : "Unknown error"}`;
1124
1316
  await callback?.({
1125
1317
  text: errorMessage,
@@ -1135,18 +1327,49 @@ The issue has been moved to the archived state and will no longer appear in acti
1135
1327
 
1136
1328
  // src/actions/getActivity.ts
1137
1329
  import {
1138
- logger as logger5,
1330
+ logger as logger6,
1139
1331
  ModelType as ModelType4
1140
1332
  } from "@elizaos/core";
1333
+ function formatActivityDetail(value) {
1334
+ if (value === null || value === undefined) {
1335
+ return "none";
1336
+ }
1337
+ if (Array.isArray(value)) {
1338
+ return value.map(formatActivityDetail).join(", ");
1339
+ }
1340
+ if (typeof value === "object") {
1341
+ return Object.entries(value).map(([key, nestedValue]) => `${key}=${formatActivityDetail(nestedValue)}`).join("; ");
1342
+ }
1343
+ return String(value);
1344
+ }
1141
1345
  var getActivityAction = {
1142
1346
  name: "GET_LINEAR_ACTIVITY",
1347
+ contexts: ["tasks", "connectors", "automation"],
1348
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
1349
+ roleGate: { minRole: "USER" },
1143
1350
  description: "Get recent Linear activity log with optional filters",
1351
+ descriptionCompressed: "get recent Linear activity log w/ optional filter",
1144
1352
  similes: [
1145
1353
  "get-linear-activity",
1146
1354
  "show-linear-activity",
1147
1355
  "view-linear-activity",
1148
1356
  "check-linear-activity"
1149
1357
  ],
1358
+ parameters: [
1359
+ {
1360
+ name: "filters",
1361
+ description: "Optional activity filters, e.g. fromDate ISO timestamp, action, resource_type, resource_id, or success.",
1362
+ required: false,
1363
+ schema: { type: "object" }
1364
+ },
1365
+ {
1366
+ name: "limit",
1367
+ description: "Maximum number of activity log entries to return.",
1368
+ required: false,
1369
+ schema: { type: "number" }
1370
+ },
1371
+ linearAccountIdParameter
1372
+ ],
1150
1373
  examples: [
1151
1374
  [
1152
1375
  {
@@ -1194,82 +1417,21 @@ var getActivityAction = {
1194
1417
  }
1195
1418
  ]
1196
1419
  ],
1197
- validate: async (runtime, message, state, options) => {
1198
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
1199
- const __avText = __avTextRaw.toLowerCase();
1200
- const __avKeywords = ["get", "linear", "activity"];
1201
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
1202
- const __avRegex = /\b(?:get|linear|activity)\b/i;
1203
- const __avRegexOk = __avRegex.test(__avText);
1204
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
1205
- const __avExpectedSource = "";
1206
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
1207
- const __avOptions = options && typeof options === "object" ? options : {};
1208
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
1209
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
1210
- return false;
1211
- }
1212
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
1213
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
1214
- const __avText2 = __avTextRaw2.toLowerCase();
1215
- const __avKeywords2 = ["get", "linear", "activity"];
1216
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
1217
- const __avRegex2 = /\b(?:get|linear|activity)\b/i;
1218
- const __avRegexOk2 = __avRegex2.test(__avText2);
1219
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
1220
- const __avExpectedSource2 = "";
1221
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
1222
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
1223
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
1224
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
1225
- return false;
1226
- }
1227
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
1228
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
1229
- const __avText3 = __avTextRaw3.toLowerCase();
1230
- const __avKeywords3 = ["get", "linear", "activity"];
1231
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
1232
- const __avRegex3 = /\b(?:get|linear|activity)\b/i;
1233
- const __avRegexOk3 = __avRegex3.test(__avText3);
1234
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
1235
- const __avExpectedSource3 = "";
1236
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
1237
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
1238
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
1239
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
1240
- return false;
1241
- }
1242
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
1243
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
1244
- return !!apiKey;
1245
- };
1246
- try {
1247
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
1248
- } catch {
1249
- return false;
1250
- }
1251
- };
1252
- try {
1253
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
1254
- } catch {
1255
- return false;
1256
- }
1257
- };
1258
- try {
1259
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
1260
- } catch {
1261
- return false;
1262
- }
1263
- },
1420
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
1421
+ keywords: ["get", "linear", "activity"],
1422
+ regexAlternation: "get|linear|activity"
1423
+ }),
1264
1424
  async handler(runtime, message, _state, _options, callback) {
1265
1425
  try {
1266
1426
  const linearService = runtime.getService("linear");
1267
1427
  if (!linearService) {
1268
1428
  throw new Error("Linear service not available");
1269
1429
  }
1430
+ const accountId = getLinearAccountId(runtime, _options);
1270
1431
  const content = message.content.text || "";
1271
- const filters = {};
1272
- let limit = 10;
1432
+ const params = _options?.parameters ?? {};
1433
+ const filters = { ...params.filters ?? {} };
1434
+ let limit = typeof params.limit === "number" && Number.isFinite(params.limit) ? Math.max(1, Math.floor(params.limit)) : 10;
1273
1435
  if (content) {
1274
1436
  const prompt = getActivityTemplate.replace("{{userMessage}}", content);
1275
1437
  const response = await runtime.useModel(ModelType4.TEXT_LARGE, {
@@ -1277,14 +1439,20 @@ var getActivityAction = {
1277
1439
  });
1278
1440
  if (response) {
1279
1441
  try {
1280
- const parsed = JSON.parse(response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
1281
- if (parsed.timeRange) {
1442
+ const parsed = parseLinearPromptResponse(response);
1443
+ if (Object.keys(parsed).length === 0) {
1444
+ throw new Error("No fields found in model response");
1445
+ }
1446
+ const timeRange = getRecordValue(parsed.timeRange);
1447
+ if (timeRange) {
1282
1448
  const now = new Date;
1283
1449
  let fromDate;
1284
- if (parsed.timeRange.from) {
1285
- fromDate = new Date(parsed.timeRange.from);
1286
- } else if (parsed.timeRange.period) {
1287
- switch (parsed.timeRange.period) {
1450
+ const from = getStringValue(timeRange.from);
1451
+ const period = getStringValue(timeRange.period);
1452
+ if (from) {
1453
+ fromDate = new Date(from);
1454
+ } else if (period) {
1455
+ switch (period) {
1288
1456
  case "today":
1289
1457
  fromDate = new Date(now.setHours(0, 0, 0, 0));
1290
1458
  break;
@@ -1309,25 +1477,29 @@ var getActivityAction = {
1309
1477
  filters.fromDate = fromDate.toISOString();
1310
1478
  }
1311
1479
  }
1312
- if (parsed.actionTypes && parsed.actionTypes.length > 0) {
1313
- filters.action = parsed.actionTypes[0];
1480
+ const actionTypes = getStringArrayValue(parsed.actionTypes);
1481
+ if (actionTypes && actionTypes.length > 0) {
1482
+ filters.action = actionTypes[0];
1314
1483
  }
1315
- if (parsed.resourceTypes && parsed.resourceTypes.length > 0) {
1316
- filters.resource_type = parsed.resourceTypes[0];
1484
+ const resourceTypes = getStringArrayValue(parsed.resourceTypes);
1485
+ if (resourceTypes && resourceTypes.length > 0) {
1486
+ filters.resource_type = resourceTypes[0];
1317
1487
  }
1318
- if (parsed.resourceId) {
1319
- filters.resource_id = parsed.resourceId;
1488
+ const resourceId = getStringValue(parsed.resourceId);
1489
+ if (resourceId) {
1490
+ filters.resource_id = resourceId;
1320
1491
  }
1321
- if (parsed.successFilter && parsed.successFilter !== "all") {
1322
- filters.success = parsed.successFilter === "success";
1492
+ const successFilter = getStringValue(parsed.successFilter);
1493
+ if (successFilter && successFilter !== "all") {
1494
+ filters.success = successFilter === "success";
1323
1495
  }
1324
- limit = parsed.limit || 10;
1496
+ limit = getNumberValue(parsed.limit) || 10;
1325
1497
  } catch (parseError) {
1326
- logger5.warn("Failed to parse activity filters:", parseError);
1498
+ logger6.warn("Failed to parse activity filters:", parseError);
1327
1499
  }
1328
1500
  }
1329
1501
  }
1330
- let activity = linearService.getActivityLog(limit * 2, filters);
1502
+ let activity = linearService.getActivityLog(limit * 2, filters, accountId);
1331
1503
  if (filters.fromDate) {
1332
1504
  const fromDateValue = filters.fromDate;
1333
1505
  const fromDate = typeof fromDateValue === "string" ? fromDateValue : fromDateValue instanceof Date ? fromDateValue.toISOString() : String(fromDateValue);
@@ -1347,14 +1519,15 @@ var getActivityAction = {
1347
1519
  text: noActivityMessage,
1348
1520
  success: true,
1349
1521
  data: {
1350
- activity: []
1522
+ activity: [],
1523
+ accountId
1351
1524
  }
1352
1525
  };
1353
1526
  }
1354
1527
  const activityText = activity.map((item, index) => {
1355
1528
  const time = new Date(item.timestamp).toLocaleString();
1356
1529
  const status = item.success ? "✅" : "❌";
1357
- const details = Object.entries(item.details).filter(([key]) => key !== "filters").map(([key, value]) => `${key}: ${JSON.stringify(value)}`).join(", ");
1530
+ const details = Object.entries(item.details).filter(([key]) => key !== "filters").map(([key, value]) => `${key}: ${formatActivityDetail(value)}`).join(", ");
1358
1531
  return `${index + 1}. ${status} ${item.action} on ${item.resource_type} ${item.resource_id}
1359
1532
  Time: ${time}
1360
1533
  ${details ? `Details: ${details}` : ""}${item.error ? `
@@ -1381,18 +1554,19 @@ ${activityText}`;
1381
1554
  resource_id: item.resource_id,
1382
1555
  success: item.success,
1383
1556
  error: item.error,
1384
- details: JSON.stringify(item.details),
1557
+ details: formatActivityDetail(item.details),
1385
1558
  timestamp: typeof item.timestamp === "string" ? item.timestamp : new Date(item.timestamp).toISOString()
1386
1559
  })),
1387
1560
  filters: filters ? {
1388
1561
  ...filters,
1389
1562
  fromDate: filters.fromDate ? typeof filters.fromDate === "string" ? filters.fromDate : String(filters.fromDate) : undefined
1390
1563
  } : undefined,
1391
- count: activity.length
1564
+ count: activity.length,
1565
+ accountId
1392
1566
  }
1393
1567
  };
1394
1568
  } catch (error) {
1395
- logger5.error("Failed to get activity:", error);
1569
+ logger6.error("Failed to get activity:", error);
1396
1570
  const errorMessage = `❌ Failed to get activity: ${error instanceof Error ? error.message : "Unknown error"}`;
1397
1571
  await callback?.({
1398
1572
  text: errorMessage,
@@ -1408,12 +1582,16 @@ ${activityText}`;
1408
1582
 
1409
1583
  // src/actions/getIssue.ts
1410
1584
  import {
1411
- logger as logger6,
1585
+ logger as logger7,
1412
1586
  ModelType as ModelType5
1413
1587
  } from "@elizaos/core";
1414
1588
  var getIssueAction = {
1415
1589
  name: "GET_LINEAR_ISSUE",
1590
+ contexts: ["tasks", "connectors", "knowledge"],
1591
+ contextGate: { anyOf: ["tasks", "connectors", "knowledge"] },
1592
+ roleGate: { minRole: "USER" },
1416
1593
  description: "Get details of a specific Linear issue",
1594
+ descriptionCompressed: "get detail specific Linear issue",
1417
1595
  similes: [
1418
1596
  "get-linear-issue",
1419
1597
  "show-linear-issue",
@@ -1421,6 +1599,21 @@ var getIssueAction = {
1421
1599
  "check-linear-issue",
1422
1600
  "find-linear-issue"
1423
1601
  ],
1602
+ parameters: [
1603
+ {
1604
+ name: "issueId",
1605
+ description: "Linear issue identifier or id, e.g. ENG-123.",
1606
+ required: false,
1607
+ schema: { type: "string" }
1608
+ },
1609
+ {
1610
+ name: "query",
1611
+ description: "Search text when the exact Linear issue identifier is unknown.",
1612
+ required: false,
1613
+ schema: { type: "string" }
1614
+ },
1615
+ linearAccountIdParameter
1616
+ ],
1424
1617
  examples: [
1425
1618
  [
1426
1619
  {
@@ -1468,80 +1661,19 @@ var getIssueAction = {
1468
1661
  }
1469
1662
  ]
1470
1663
  ],
1471
- validate: async (runtime, message, state, options) => {
1472
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
1473
- const __avText = __avTextRaw.toLowerCase();
1474
- const __avKeywords = ["get", "linear", "issue"];
1475
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
1476
- const __avRegex = /\b(?:get|linear|issue)\b/i;
1477
- const __avRegexOk = __avRegex.test(__avText);
1478
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
1479
- const __avExpectedSource = "";
1480
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
1481
- const __avOptions = options && typeof options === "object" ? options : {};
1482
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
1483
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
1484
- return false;
1485
- }
1486
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
1487
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
1488
- const __avText2 = __avTextRaw2.toLowerCase();
1489
- const __avKeywords2 = ["get", "linear", "issue"];
1490
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
1491
- const __avRegex2 = /\b(?:get|linear|issue)\b/i;
1492
- const __avRegexOk2 = __avRegex2.test(__avText2);
1493
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
1494
- const __avExpectedSource2 = "";
1495
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
1496
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
1497
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
1498
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
1499
- return false;
1500
- }
1501
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
1502
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
1503
- const __avText3 = __avTextRaw3.toLowerCase();
1504
- const __avKeywords3 = ["get", "linear", "issue"];
1505
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
1506
- const __avRegex3 = /\b(?:get|linear|issue)\b/i;
1507
- const __avRegexOk3 = __avRegex3.test(__avText3);
1508
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
1509
- const __avExpectedSource3 = "";
1510
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
1511
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
1512
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
1513
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
1514
- return false;
1515
- }
1516
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
1517
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
1518
- return !!apiKey;
1519
- };
1520
- try {
1521
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
1522
- } catch {
1523
- return false;
1524
- }
1525
- };
1526
- try {
1527
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
1528
- } catch {
1529
- return false;
1530
- }
1531
- };
1532
- try {
1533
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
1534
- } catch {
1535
- return false;
1536
- }
1537
- },
1664
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
1665
+ keywords: ["get", "linear", "issue"],
1666
+ regexAlternation: "get|linear|issue"
1667
+ }),
1538
1668
  async handler(runtime, message, _state, _options, callback) {
1539
1669
  try {
1540
1670
  const linearService = runtime.getService("linear");
1541
1671
  if (!linearService) {
1542
1672
  throw new Error("Linear service not available");
1543
1673
  }
1544
- const content = message.content.text;
1674
+ const accountId = getLinearAccountId(runtime, _options);
1675
+ const params = _options?.parameters ?? {};
1676
+ const content = params.query ?? params.issueId ?? message.content.text;
1545
1677
  if (!content) {
1546
1678
  const errorMessage2 = "Please specify which issue you want to see.";
1547
1679
  await callback?.({
@@ -1553,6 +1685,10 @@ var getIssueAction = {
1553
1685
  success: false
1554
1686
  };
1555
1687
  }
1688
+ if (params.issueId) {
1689
+ const issue = await linearService.getIssue(params.issueId, accountId);
1690
+ return await formatIssueResponse(issue, callback, message);
1691
+ }
1556
1692
  const prompt = getIssueTemplate.replace("{{userMessage}}", content);
1557
1693
  const response = await runtime.useModel(ModelType5.TEXT_LARGE, {
1558
1694
  prompt
@@ -1560,26 +1696,34 @@ var getIssueAction = {
1560
1696
  if (!response) {
1561
1697
  const issueMatch = content.match(/(\w+-\d+)/);
1562
1698
  if (issueMatch) {
1563
- const issue = await linearService.getIssue(issueMatch[1]);
1699
+ const issue = await linearService.getIssue(issueMatch[1], accountId);
1564
1700
  return await formatIssueResponse(issue, callback, message);
1565
1701
  }
1566
1702
  throw new Error("Could not understand issue reference");
1567
1703
  }
1568
1704
  try {
1569
- const parsed = JSON.parse(response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
1570
- if (parsed.directId) {
1571
- const issue = await linearService.getIssue(parsed.directId);
1705
+ const parsed = parseLinearPromptResponse(response);
1706
+ if (Object.keys(parsed).length === 0) {
1707
+ throw new Error("No fields found in model response");
1708
+ }
1709
+ const directId = getStringValue(parsed.directId);
1710
+ if (directId) {
1711
+ const issue = await linearService.getIssue(directId, accountId);
1572
1712
  return await formatIssueResponse(issue, callback, message);
1573
1713
  }
1574
- if (parsed.searchBy && Object.keys(parsed.searchBy).length > 0) {
1714
+ const searchBy = getRecordValue(parsed.searchBy);
1715
+ if (searchBy && Object.keys(searchBy).length > 0) {
1575
1716
  const filters = {};
1576
- if (parsed.searchBy.title) {
1577
- filters.query = parsed.searchBy.title;
1717
+ const title = getStringValue(searchBy.title);
1718
+ if (title) {
1719
+ filters.query = title;
1578
1720
  }
1579
- if (parsed.searchBy.assignee) {
1580
- filters.assignee = [parsed.searchBy.assignee];
1721
+ const assignee = getStringValue(searchBy.assignee);
1722
+ if (assignee) {
1723
+ filters.assignee = [assignee];
1581
1724
  }
1582
- if (parsed.searchBy.priority) {
1725
+ const priorityValue = getStringValue(searchBy.priority);
1726
+ if (priorityValue) {
1583
1727
  const priorityMap = {
1584
1728
  urgent: 1,
1585
1729
  high: 2,
@@ -1590,25 +1734,27 @@ var getIssueAction = {
1590
1734
  "3": 3,
1591
1735
  "4": 4
1592
1736
  };
1593
- const priority = priorityMap[parsed.searchBy.priority.toLowerCase()];
1737
+ const priority = priorityMap[priorityValue.toLowerCase()];
1594
1738
  if (priority) {
1595
1739
  filters.priority = [priority];
1596
1740
  }
1597
1741
  }
1598
- if (parsed.searchBy.team) {
1599
- filters.team = parsed.searchBy.team;
1742
+ const team = getStringValue(searchBy.team);
1743
+ if (team) {
1744
+ filters.team = team;
1600
1745
  }
1601
- if (parsed.searchBy.state) {
1602
- filters.state = [parsed.searchBy.state];
1746
+ const state = getStringValue(searchBy.state);
1747
+ if (state) {
1748
+ filters.state = [state];
1603
1749
  }
1604
- const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
1750
+ const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
1605
1751
  if (defaultTeamKey && !filters.team) {
1606
1752
  filters.team = defaultTeamKey;
1607
1753
  }
1608
1754
  const issues = await linearService.searchIssues({
1609
1755
  ...filters,
1610
- limit: parsed.searchBy.recency ? 10 : 5
1611
- });
1756
+ limit: getStringValue(searchBy.recency) ? 10 : 5
1757
+ }, accountId);
1612
1758
  if (issues.length === 0) {
1613
1759
  const noResultsMessage = "No issues found matching your criteria.";
1614
1760
  await callback?.({
@@ -1620,18 +1766,18 @@ var getIssueAction = {
1620
1766
  success: false
1621
1767
  };
1622
1768
  }
1623
- if (parsed.searchBy.recency) {
1769
+ if (getStringValue(searchBy.recency)) {
1624
1770
  issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
1625
1771
  }
1626
- if (parsed.searchBy.recency && issues.length > 0) {
1772
+ if (getStringValue(searchBy.recency) && issues.length > 0) {
1627
1773
  return await formatIssueResponse(issues[0], callback, message);
1628
1774
  }
1629
1775
  if (issues.length === 1) {
1630
1776
  return await formatIssueResponse(issues[0], callback, message);
1631
1777
  }
1632
1778
  const issueList = await Promise.all(issues.slice(0, 5).map(async (issue, index) => {
1633
- const state = await issue.state;
1634
- return `${index + 1}. ${issue.identifier}: ${issue.title} (${state?.name || "No state"})`;
1779
+ const state2 = await issue.state;
1780
+ return `${index + 1}. ${issue.identifier}: ${issue.title} (${state2?.name || "No state"})`;
1635
1781
  }));
1636
1782
  const clarifyMessage = `Found ${issues.length} issues matching your criteria:
1637
1783
  ${issueList.join(`
@@ -1656,10 +1802,10 @@ Please specify which one you want to see by its ID.`;
1656
1802
  };
1657
1803
  }
1658
1804
  } catch (parseError) {
1659
- logger6.warn("Failed to parse LLM response, falling back to regex:", parseError);
1805
+ logger7.warn("Failed to parse LLM response, falling back to regex:", parseError);
1660
1806
  const issueMatch = content.match(/(\w+-\d+)/);
1661
1807
  if (issueMatch) {
1662
- const issue = await linearService.getIssue(issueMatch[1]);
1808
+ const issue = await linearService.getIssue(issueMatch[1], accountId);
1663
1809
  return await formatIssueResponse(issue, callback, message);
1664
1810
  }
1665
1811
  }
@@ -1673,7 +1819,7 @@ Please specify which one you want to see by its ID.`;
1673
1819
  success: false
1674
1820
  };
1675
1821
  } catch (error) {
1676
- logger6.error("Failed to get issue:", error);
1822
+ logger7.error("Failed to get issue:", error);
1677
1823
  const errorMessage = `❌ Failed to get issue: ${error instanceof Error ? error.message : "Unknown error"}`;
1678
1824
  await callback?.({
1679
1825
  text: errorMessage,
@@ -1764,573 +1910,148 @@ View in Linear: ${issue.url}`;
1764
1910
  };
1765
1911
  }
1766
1912
 
1767
- // src/actions/listProjects.ts
1913
+ // src/actions/listComments.ts
1768
1914
  import {
1769
- logger as logger7,
1770
- ModelType as ModelType6
1915
+ logger as logger8
1771
1916
  } from "@elizaos/core";
1772
- var listProjectsAction = {
1773
- name: "LIST_LINEAR_PROJECTS",
1774
- description: "List projects in Linear with optional filters",
1775
- similes: [
1776
- "list-linear-projects",
1777
- "show-linear-projects",
1778
- "get-linear-projects",
1779
- "view-linear-projects"
1917
+ var listCommentsAction = {
1918
+ name: "LIST_LINEAR_COMMENTS",
1919
+ contexts: ["tasks", "connectors", "automation"],
1920
+ contextGate: { anyOf: ["tasks", "connectors", "automation"] },
1921
+ roleGate: { minRole: "USER" },
1922
+ description: "List comments on a Linear issue",
1923
+ descriptionCompressed: "list comment Linear issue",
1924
+ parameters: [
1925
+ {
1926
+ name: "issueId",
1927
+ description: "Linear issue id or identifier to list comments for.",
1928
+ required: false,
1929
+ schema: { type: "string" }
1930
+ },
1931
+ {
1932
+ name: "limit",
1933
+ description: "Maximum number of comments to return (default 25, max 100).",
1934
+ required: false,
1935
+ schema: { type: "number" }
1936
+ },
1937
+ linearAccountIdParameter
1780
1938
  ],
1939
+ similes: ["get-linear-comments", "show-linear-comments", "fetch-linear-comments"],
1781
1940
  examples: [
1782
1941
  [
1783
1942
  {
1784
1943
  name: "User",
1785
- content: {
1786
- text: "Show me all projects"
1787
- }
1788
- },
1789
- {
1790
- name: "Assistant",
1791
- content: {
1792
- text: "I'll list all the projects in Linear for you.",
1793
- actions: ["LIST_LINEAR_PROJECTS"]
1794
- }
1795
- }
1796
- ],
1797
- [
1798
- {
1799
- name: "User",
1800
- content: {
1801
- text: "What active projects do we have?"
1802
- }
1803
- },
1804
- {
1805
- name: "Assistant",
1806
- content: {
1807
- text: "Let me show you all the active projects.",
1808
- actions: ["LIST_LINEAR_PROJECTS"]
1809
- }
1810
- }
1811
- ],
1812
- [
1813
- {
1814
- name: "User",
1815
- content: {
1816
- text: "Show me projects for the engineering team"
1817
- }
1944
+ content: { text: "Show the comments on ENG-123." }
1818
1945
  },
1819
1946
  {
1820
1947
  name: "Assistant",
1821
1948
  content: {
1822
- text: "I'll find the projects for the engineering team.",
1823
- actions: ["LIST_LINEAR_PROJECTS"]
1949
+ text: "Here are the comments on ENG-123.",
1950
+ actions: ["LIST_LINEAR_COMMENTS"]
1824
1951
  }
1825
1952
  }
1826
1953
  ]
1827
1954
  ],
1828
- validate: async (runtime, message, state, options) => {
1829
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
1830
- const __avText = __avTextRaw.toLowerCase();
1831
- const __avKeywords = ["list", "linear", "projects"];
1832
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
1833
- const __avRegex = /\b(?:list|linear|projects)\b/i;
1834
- const __avRegexOk = __avRegex.test(__avText);
1835
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
1836
- const __avExpectedSource = "";
1837
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
1838
- const __avOptions = options && typeof options === "object" ? options : {};
1839
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
1840
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
1841
- return false;
1842
- }
1843
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
1844
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
1845
- const __avText2 = __avTextRaw2.toLowerCase();
1846
- const __avKeywords2 = ["list", "linear", "projects"];
1847
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
1848
- const __avRegex2 = /\b(?:list|linear|projects)\b/i;
1849
- const __avRegexOk2 = __avRegex2.test(__avText2);
1850
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
1851
- const __avExpectedSource2 = "";
1852
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
1853
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
1854
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
1855
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
1856
- return false;
1857
- }
1858
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
1859
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
1860
- const __avText3 = __avTextRaw3.toLowerCase();
1861
- const __avKeywords3 = ["list", "linear", "projects"];
1862
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
1863
- const __avRegex3 = /\b(?:list|linear|projects)\b/i;
1864
- const __avRegexOk3 = __avRegex3.test(__avText3);
1865
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
1866
- const __avExpectedSource3 = "";
1867
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
1868
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
1869
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
1870
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
1871
- return false;
1872
- }
1873
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
1874
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
1875
- return !!apiKey;
1876
- };
1877
- try {
1878
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
1879
- } catch {
1880
- return false;
1881
- }
1882
- };
1883
- try {
1884
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
1885
- } catch {
1886
- return false;
1887
- }
1888
- };
1889
- try {
1890
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
1891
- } catch {
1892
- return false;
1893
- }
1894
- },
1955
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
1956
+ keywords: ["list", "comments", "linear", "issue"],
1957
+ regexAlternation: "list|comments|linear|issue"
1958
+ }),
1895
1959
  async handler(runtime, message, _state, _options, callback) {
1896
1960
  try {
1897
1961
  const linearService = runtime.getService("linear");
1898
1962
  if (!linearService) {
1899
1963
  throw new Error("Linear service not available");
1900
1964
  }
1901
- const content = message.content.text || "";
1902
- let teamId;
1903
- let showAll = false;
1904
- let stateFilter;
1905
- if (content) {
1906
- const prompt = listProjectsTemplate.replace("{{userMessage}}", content);
1907
- const response = await runtime.useModel(ModelType6.TEXT_LARGE, {
1908
- prompt
1909
- });
1910
- if (response) {
1911
- try {
1912
- const parsed = JSON.parse(response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
1913
- if (parsed.teamFilter) {
1914
- const teams = await linearService.getTeams();
1915
- const team = teams.find((t) => t.key.toLowerCase() === parsed.teamFilter.toLowerCase() || t.name.toLowerCase() === parsed.teamFilter.toLowerCase());
1916
- if (team) {
1917
- teamId = team.id;
1918
- logger7.info(`Filtering projects by team: ${team.name} (${team.key})`);
1919
- }
1920
- }
1921
- showAll = parsed.showAll === true;
1922
- stateFilter = parsed.stateFilter;
1923
- } catch (parseError) {
1924
- logger7.warn("Failed to parse project filters, using basic parsing:", parseError);
1925
- const teamMatch = content.match(/(?:for|in|of)\s+(?:the\s+)?(\w+)\s+team/i);
1926
- if (teamMatch) {
1927
- const teams = await linearService.getTeams();
1928
- const team = teams.find((t) => t.key.toLowerCase() === teamMatch[1].toLowerCase() || t.name.toLowerCase() === teamMatch[1].toLowerCase());
1929
- if (team) {
1930
- teamId = team.id;
1931
- logger7.info(`Filtering projects by team: ${team.name} (${team.key})`);
1932
- }
1933
- }
1934
- showAll = content.toLowerCase().includes("all") && content.toLowerCase().includes("project");
1935
- }
1936
- }
1937
- }
1938
- if (!teamId && !showAll) {
1939
- const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
1940
- if (defaultTeamKey) {
1941
- const teams = await linearService.getTeams();
1942
- const defaultTeam = teams.find((t) => t.key.toLowerCase() === defaultTeamKey.toLowerCase());
1943
- if (defaultTeam) {
1944
- teamId = defaultTeam.id;
1945
- logger7.info(`Applying default team filter for projects: ${defaultTeam.name} (${defaultTeam.key})`);
1946
- }
1965
+ const accountId = getLinearAccountId(runtime, _options);
1966
+ const params = _options?.parameters;
1967
+ const issueId = params?.issueId?.trim() ?? "";
1968
+ const limit = typeof params?.limit === "number" ? Math.min(Math.max(1, params.limit), 100) : 25;
1969
+ if (!issueId) {
1970
+ const match = message.content.text?.match(/([A-Z]+-\d+)/);
1971
+ if (!match) {
1972
+ const errorMessage = "Please provide an issueId to list comments for.";
1973
+ await callback?.({
1974
+ text: errorMessage,
1975
+ source: message.content.source
1976
+ });
1977
+ return { text: errorMessage, success: false };
1947
1978
  }
1979
+ const extractedId = match[1];
1980
+ const comments2 = await linearService.listComments(extractedId, limit, accountId);
1981
+ return formatCommentResult(extractedId, comments2, message, callback);
1948
1982
  }
1949
- let projects = await linearService.getProjects(teamId);
1950
- if (stateFilter && stateFilter !== "all") {
1951
- projects = projects.filter((project) => {
1952
- const state = project.state?.toLowerCase() || "";
1953
- if (stateFilter === "active") {
1954
- return state === "started" || state === "in progress" || !state;
1955
- } else if (stateFilter === "planned") {
1956
- return state === "planned" || state === "backlog";
1957
- } else if (stateFilter === "completed") {
1958
- return state === "completed" || state === "done" || state === "canceled";
1959
- }
1960
- return true;
1961
- });
1962
- }
1963
- if (projects.length === 0) {
1964
- const noProjectsMessage = teamId ? "No projects found for the specified team." : "No projects found in Linear.";
1965
- await callback?.({
1966
- text: noProjectsMessage,
1967
- source: message.content.source
1968
- });
1969
- return {
1970
- text: noProjectsMessage,
1971
- success: true,
1972
- data: {
1973
- projects: []
1974
- }
1975
- };
1976
- }
1977
- const projectsWithDetails = await Promise.all(projects.map(async (project) => {
1978
- const teamsQuery = await project.teams();
1979
- const teams = await teamsQuery.nodes;
1980
- const lead = await project.lead;
1981
- return {
1982
- ...project,
1983
- teamsList: teams,
1984
- leadUser: lead
1985
- };
1986
- }));
1987
- const projectList = projectsWithDetails.map((project, index) => {
1988
- const teamNames = project.teamsList.map((t) => t.name).join(", ") || "No teams";
1989
- const status = project.state || "Active";
1990
- const progress = project.progress ? ` (${Math.round(project.progress * 100)}% complete)` : "";
1991
- const lead = project.leadUser ? ` | Lead: ${project.leadUser.name}` : "";
1992
- const dates = [];
1993
- if (project.startDate)
1994
- dates.push(`Start: ${new Date(project.startDate).toLocaleDateString()}`);
1995
- if (project.targetDate)
1996
- dates.push(`Due: ${new Date(project.targetDate).toLocaleDateString()}`);
1997
- const dateInfo = dates.length > 0 ? `
1998
- ${dates.join(" | ")}` : "";
1999
- return `${index + 1}. ${project.name}${project.description ? ` - ${project.description}` : ""}
2000
- Status: ${status}${progress} | Teams: ${teamNames}${lead}${dateInfo}`;
2001
- }).join(`
2002
-
2003
- `);
2004
- const headerText = stateFilter && stateFilter !== "all" ? `\uD83D\uDCC1 Found ${projects.length} ${stateFilter} project${projects.length === 1 ? "" : "s"}:` : `\uD83D\uDCC1 Found ${projects.length} project${projects.length === 1 ? "" : "s"}:`;
2005
- const resultMessage = `${headerText}
2006
-
2007
- ${projectList}`;
2008
- await callback?.({
2009
- text: resultMessage,
2010
- source: message.content.source
2011
- });
2012
- return {
2013
- text: `Found ${projects.length} project${projects.length === 1 ? "" : "s"}`,
2014
- success: true,
2015
- data: {
2016
- projects: projectsWithDetails.map((p) => ({
2017
- id: p.id,
2018
- name: p.name,
2019
- description: p.description,
2020
- url: p.url,
2021
- teams: p.teamsList.map((t) => ({
2022
- id: t.id,
2023
- name: t.name,
2024
- key: t.key
2025
- })),
2026
- lead: p.leadUser ? {
2027
- id: p.leadUser.id,
2028
- name: p.leadUser.name,
2029
- email: p.leadUser.email
2030
- } : null,
2031
- state: p.state,
2032
- progress: p.progress,
2033
- startDate: p.startDate,
2034
- targetDate: p.targetDate
2035
- })),
2036
- count: projects.length,
2037
- filters: {
2038
- team: teamId,
2039
- state: stateFilter
2040
- }
2041
- }
2042
- };
1983
+ const comments = await linearService.listComments(issueId, limit, accountId);
1984
+ return formatCommentResult(issueId, comments, message, callback);
2043
1985
  } catch (error) {
2044
- logger7.error("Failed to list projects:", error);
2045
- const errorMessage = `❌ Failed to list projects: ${error instanceof Error ? error.message : "Unknown error"}`;
2046
- await callback?.({
2047
- text: errorMessage,
2048
- source: message.content.source
2049
- });
2050
- return {
2051
- text: errorMessage,
2052
- success: false
2053
- };
1986
+ logger8.error("Failed to list comments:", error);
1987
+ const errorMessage = `Failed to list comments: ${error instanceof Error ? error.message : "Unknown error"}`;
1988
+ await callback?.({ text: errorMessage, source: message.content.source });
1989
+ return { text: errorMessage, success: false };
2054
1990
  }
2055
1991
  }
2056
1992
  };
2057
-
2058
- // src/actions/listTeams.ts
2059
- import {
2060
- logger as logger8,
2061
- ModelType as ModelType7
2062
- } from "@elizaos/core";
2063
- var listTeamsAction = {
2064
- name: "LIST_LINEAR_TEAMS",
2065
- description: "List teams in Linear with optional filters",
2066
- similes: ["list-linear-teams", "show-linear-teams", "get-linear-teams", "view-linear-teams"],
2067
- examples: [
2068
- [
2069
- {
2070
- name: "User",
2071
- content: {
2072
- text: "Show me all teams"
2073
- }
2074
- },
2075
- {
2076
- name: "Assistant",
2077
- content: {
2078
- text: "I'll list all the teams in Linear for you.",
2079
- actions: ["LIST_LINEAR_TEAMS"]
2080
- }
2081
- }
2082
- ],
2083
- [
2084
- {
2085
- name: "User",
2086
- content: {
2087
- text: "Which engineering teams do we have?"
2088
- }
2089
- },
2090
- {
2091
- name: "Assistant",
2092
- content: {
2093
- text: "Let me find the engineering teams for you.",
2094
- actions: ["LIST_LINEAR_TEAMS"]
2095
- }
2096
- }
2097
- ],
2098
- [
2099
- {
2100
- name: "User",
2101
- content: {
2102
- text: "Show me the teams I'm part of"
2103
- }
2104
- },
2105
- {
2106
- name: "Assistant",
2107
- content: {
2108
- text: "I'll show you the teams you're a member of.",
2109
- actions: ["LIST_LINEAR_TEAMS"]
2110
- }
2111
- }
2112
- ]
2113
- ],
2114
- validate: async (runtime, message, state, options) => {
2115
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
2116
- const __avText = __avTextRaw.toLowerCase();
2117
- const __avKeywords = ["list", "linear", "teams"];
2118
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
2119
- const __avRegex = /\b(?:list|linear|teams)\b/i;
2120
- const __avRegexOk = __avRegex.test(__avText);
2121
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
2122
- const __avExpectedSource = "";
2123
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
2124
- const __avOptions = options && typeof options === "object" ? options : {};
2125
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
2126
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
2127
- return false;
2128
- }
2129
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
2130
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
2131
- const __avText2 = __avTextRaw2.toLowerCase();
2132
- const __avKeywords2 = ["list", "linear", "teams"];
2133
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
2134
- const __avRegex2 = /\b(?:list|linear|teams)\b/i;
2135
- const __avRegexOk2 = __avRegex2.test(__avText2);
2136
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
2137
- const __avExpectedSource2 = "";
2138
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
2139
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
2140
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
2141
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
2142
- return false;
2143
- }
2144
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
2145
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
2146
- const __avText3 = __avTextRaw3.toLowerCase();
2147
- const __avKeywords3 = ["list", "linear", "teams"];
2148
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
2149
- const __avRegex3 = /\b(?:list|linear|teams)\b/i;
2150
- const __avRegexOk3 = __avRegex3.test(__avText3);
2151
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
2152
- const __avExpectedSource3 = "";
2153
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
2154
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
2155
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
2156
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
2157
- return false;
2158
- }
2159
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
2160
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
2161
- return !!apiKey;
2162
- };
2163
- try {
2164
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
2165
- } catch {
2166
- return false;
2167
- }
2168
- };
2169
- try {
2170
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
2171
- } catch {
2172
- return false;
2173
- }
2174
- };
2175
- try {
2176
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
2177
- } catch {
2178
- return false;
2179
- }
2180
- },
2181
- async handler(runtime, message, _state, _options, callback) {
2182
- try {
2183
- const linearService = runtime.getService("linear");
2184
- if (!linearService) {
2185
- throw new Error("Linear service not available");
2186
- }
2187
- const content = message.content.text || "";
2188
- let nameFilter;
2189
- let specificTeam;
2190
- let myTeams = false;
2191
- let includeDetails = false;
2192
- if (content) {
2193
- const prompt = listTeamsTemplate.replace("{{userMessage}}", content);
2194
- const response = await runtime.useModel(ModelType7.TEXT_LARGE, {
2195
- prompt
2196
- });
2197
- if (response) {
2198
- try {
2199
- const parsed = JSON.parse(response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
2200
- nameFilter = parsed.nameFilter;
2201
- specificTeam = parsed.specificTeam;
2202
- myTeams = parsed.myTeams === true;
2203
- includeDetails = parsed.includeDetails === true;
2204
- } catch (parseError) {
2205
- logger8.warn("Failed to parse team filters:", parseError);
2206
- }
2207
- }
2208
- }
2209
- let teams = await linearService.getTeams();
2210
- if (specificTeam) {
2211
- teams = teams.filter((team) => team.key.toLowerCase() === specificTeam.toLowerCase() || team.name.toLowerCase() === specificTeam.toLowerCase());
2212
- }
2213
- if (nameFilter && !specificTeam) {
2214
- const keywords = nameFilter.toLowerCase().split(/\s+/);
2215
- teams = teams.filter((team) => {
2216
- const teamText = `${team.name} ${team.description || ""}`.toLowerCase();
2217
- return keywords.some((keyword) => teamText.includes(keyword));
2218
- });
2219
- }
2220
- if (myTeams) {
2221
- try {
2222
- const userTeams = await linearService.getUserTeams();
2223
- const userTeamIds = new Set(userTeams.map((t) => t.id));
2224
- teams = teams.filter((team) => userTeamIds.has(team.id));
2225
- } catch (error) {
2226
- logger8.warn("Could not filter for user's teams:", error);
2227
- }
2228
- }
2229
- if (teams.length === 0) {
2230
- const noTeamsMessage = specificTeam ? `No team found matching "${specificTeam}".` : nameFilter ? `No teams found matching "${nameFilter}".` : "No teams found in Linear.";
2231
- await callback?.({
2232
- text: noTeamsMessage,
2233
- source: message.content.source
2234
- });
2235
- return {
2236
- text: noTeamsMessage,
2237
- success: true,
2238
- data: {
2239
- teams: []
2240
- }
2241
- };
2242
- }
2243
- let teamsWithDetails = teams;
2244
- if (includeDetails || specificTeam) {
2245
- teamsWithDetails = await Promise.all(teams.map(async (team) => {
2246
- const membersQuery = await team.members();
2247
- const members = await membersQuery.nodes;
2248
- const projectsQuery = await team.projects();
2249
- const projects = await projectsQuery.nodes;
2250
- return Object.assign(team, {
2251
- memberCount: members.length,
2252
- projectCount: projects.length,
2253
- membersList: specificTeam ? members.slice(0, 5) : []
2254
- });
2255
- }));
2256
- }
2257
- const teamList = teamsWithDetails.map((team, index) => {
2258
- let info = `${index + 1}. ${team.name} (${team.key})`;
2259
- if (team.description) {
2260
- info += `
2261
- ${team.description}`;
2262
- }
2263
- if (includeDetails || specificTeam) {
2264
- info += `
2265
- Members: ${team.memberCount ?? 0} | Projects: ${team.projectCount ?? 0}`;
2266
- const membersList = team.membersList ?? [];
2267
- if (specificTeam && membersList.length > 0) {
2268
- const memberNames = membersList.map((m) => m.name).join(", ");
2269
- info += `
2270
- Team members: ${memberNames}${(team.memberCount ?? 0) > 5 ? " ..." : ""}`;
2271
- }
2272
- }
2273
- return info;
2274
- }).join(`
2275
-
2276
- `);
2277
- const headerText = specificTeam && teams.length === 1 ? `\uD83D\uDCCB Team Details:` : nameFilter ? `\uD83D\uDCCB Found ${teams.length} team${teams.length === 1 ? "" : "s"} matching "${nameFilter}":` : `\uD83D\uDCCB Found ${teams.length} team${teams.length === 1 ? "" : "s"}:`;
2278
- const resultMessage = `${headerText}
2279
-
2280
- ${teamList}`;
2281
- await callback?.({
2282
- text: resultMessage,
2283
- source: message.content.source
2284
- });
2285
- return {
2286
- text: `Found ${teams.length} team${teams.length === 1 ? "" : "s"}`,
2287
- success: true,
2288
- data: {
2289
- teams: teamsWithDetails.map((t) => ({
2290
- id: t.id,
2291
- name: t.name,
2292
- key: t.key,
2293
- description: t.description,
2294
- memberCount: t.memberCount,
2295
- projectCount: t.projectCount
2296
- })),
2297
- count: teams.length,
2298
- filters: {
2299
- name: nameFilter,
2300
- specific: specificTeam
2301
- }
2302
- }
2303
- };
2304
- } catch (error) {
2305
- logger8.error("Failed to list teams:", error);
2306
- const errorMessage = `❌ Failed to list teams: ${error instanceof Error ? error.message : "Unknown error"}`;
2307
- await callback?.({
2308
- text: errorMessage,
2309
- source: message.content.source
2310
- });
2311
- return {
2312
- text: errorMessage,
2313
- success: false
2314
- };
2315
- }
1993
+ async function formatCommentResult(issueId, comments, message, callback) {
1994
+ if (comments.length === 0) {
1995
+ const text2 = `No comments on issue ${issueId}.`;
1996
+ await callback?.({ text: text2, source: message.content.source });
1997
+ return { text: text2, success: true, data: { issueId, comments: [] } };
2316
1998
  }
2317
- };
1999
+ const lines = await Promise.all(comments.map(async (c) => {
2000
+ const user = await c.user;
2001
+ const name = user?.name ?? "unknown";
2002
+ const created = c.createdAt ? new Date(c.createdAt).toISOString().slice(0, 10) : "?";
2003
+ const body = (c.body ?? "").slice(0, 200);
2004
+ return `- [${c.id}] ${name} (${created}): ${body}`;
2005
+ }));
2006
+ const text = `${comments.length} comment(s) on ${issueId}:
2007
+ ${lines.join(`
2008
+ `)}`;
2009
+ await callback?.({ text, source: message.content.source });
2010
+ return {
2011
+ text,
2012
+ success: true,
2013
+ data: {
2014
+ issueId,
2015
+ count: comments.length,
2016
+ comments: comments.map((c) => ({ id: c.id }))
2017
+ }
2018
+ };
2019
+ }
2318
2020
 
2319
2021
  // src/actions/searchIssues.ts
2320
2022
  import {
2321
2023
  logger as logger9,
2322
- ModelType as ModelType8
2024
+ ModelType as ModelType6
2323
2025
  } from "@elizaos/core";
2324
2026
  var searchTemplate = searchIssuesTemplate;
2325
2027
  var searchIssuesAction = {
2326
2028
  name: "SEARCH_LINEAR_ISSUES",
2029
+ contexts: ["tasks", "connectors", "knowledge"],
2030
+ contextGate: { anyOf: ["tasks", "connectors", "knowledge"] },
2031
+ roleGate: { minRole: "USER" },
2327
2032
  description: "Search for issues in Linear with various filters",
2033
+ descriptionCompressed: "search issue Linear w/ various filter",
2328
2034
  similes: [
2329
2035
  "search-linear-issues",
2330
2036
  "find-linear-issues",
2331
2037
  "query-linear-issues",
2332
2038
  "list-linear-issues"
2333
2039
  ],
2040
+ parameters: [
2041
+ {
2042
+ name: "filters",
2043
+ description: "Structured Linear issue filters: query, state, assignee, priority, team, label, and limit.",
2044
+ required: false,
2045
+ schema: { type: "object" }
2046
+ },
2047
+ {
2048
+ name: "limit",
2049
+ description: "Maximum number of issues to return.",
2050
+ required: false,
2051
+ schema: { type: "number" }
2052
+ },
2053
+ linearAccountIdParameter
2054
+ ],
2334
2055
  examples: [
2335
2056
  [
2336
2057
  {
@@ -2378,79 +2099,17 @@ var searchIssuesAction = {
2378
2099
  }
2379
2100
  ]
2380
2101
  ],
2381
- validate: async (runtime, message, state, options) => {
2382
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
2383
- const __avText = __avTextRaw.toLowerCase();
2384
- const __avKeywords = ["search", "linear", "issues"];
2385
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
2386
- const __avRegex = /\b(?:search|linear|issues)\b/i;
2387
- const __avRegexOk = __avRegex.test(__avText);
2388
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
2389
- const __avExpectedSource = "";
2390
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
2391
- const __avOptions = options && typeof options === "object" ? options : {};
2392
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
2393
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
2394
- return false;
2395
- }
2396
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
2397
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
2398
- const __avText2 = __avTextRaw2.toLowerCase();
2399
- const __avKeywords2 = ["search", "linear", "issues"];
2400
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
2401
- const __avRegex2 = /\b(?:search|linear|issues)\b/i;
2402
- const __avRegexOk2 = __avRegex2.test(__avText2);
2403
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
2404
- const __avExpectedSource2 = "";
2405
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
2406
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
2407
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
2408
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
2409
- return false;
2410
- }
2411
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
2412
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
2413
- const __avText3 = __avTextRaw3.toLowerCase();
2414
- const __avKeywords3 = ["search", "linear", "issues"];
2415
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
2416
- const __avRegex3 = /\b(?:search|linear|issues)\b/i;
2417
- const __avRegexOk3 = __avRegex3.test(__avText3);
2418
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
2419
- const __avExpectedSource3 = "";
2420
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
2421
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
2422
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
2423
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
2424
- return false;
2425
- }
2426
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
2427
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
2428
- return !!apiKey;
2429
- };
2430
- try {
2431
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
2432
- } catch {
2433
- return false;
2434
- }
2435
- };
2436
- try {
2437
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
2438
- } catch {
2439
- return false;
2440
- }
2441
- };
2442
- try {
2443
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
2444
- } catch {
2445
- return false;
2446
- }
2447
- },
2102
+ validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
2103
+ keywords: ["search", "linear", "issues"],
2104
+ regexAlternation: "search|linear|issues"
2105
+ }),
2448
2106
  async handler(runtime, message, _state, _options, callback) {
2449
2107
  try {
2450
2108
  const linearService = runtime.getService("linear");
2451
2109
  if (!linearService) {
2452
2110
  throw new Error("Linear service not available");
2453
2111
  }
2112
+ const accountId = getLinearAccountId(runtime, _options);
2454
2113
  const content = message.content.text;
2455
2114
  if (!content) {
2456
2115
  const errorMessage = "Please provide search criteria for issues.";
@@ -2469,28 +2128,32 @@ var searchIssuesAction = {
2469
2128
  filters = params.filters;
2470
2129
  } else {
2471
2130
  const prompt = searchTemplate.replace("{{userMessage}}", content);
2472
- const response = await runtime.useModel(ModelType8.TEXT_LARGE, {
2131
+ const response = await runtime.useModel(ModelType6.TEXT_LARGE, {
2473
2132
  prompt
2474
2133
  });
2475
2134
  if (!response) {
2476
2135
  filters = { query: content };
2477
2136
  } else {
2478
2137
  try {
2479
- const cleanedResponse = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
2480
- const parsed = JSON.parse(cleanedResponse);
2138
+ const parsed = parseLinearPromptResponse(response);
2139
+ if (Object.keys(parsed).length === 0) {
2140
+ throw new Error("No fields found in model response");
2141
+ }
2481
2142
  filters = {
2482
- query: parsed.query,
2483
- limit: parsed.limit || 10
2143
+ query: getStringValue(parsed.query),
2144
+ limit: getNumberValue(parsed.limit) || 10
2484
2145
  };
2485
- if (parsed.states && parsed.states.length > 0) {
2486
- filters.state = parsed.states;
2146
+ const states = getStringArrayValue(parsed.states);
2147
+ if (states && states.length > 0) {
2148
+ filters.state = states;
2487
2149
  }
2488
- if (parsed.assignees && parsed.assignees.length > 0) {
2150
+ const assignees = getStringArrayValue(parsed.assignees);
2151
+ if (assignees && assignees.length > 0) {
2489
2152
  const processedAssignees = [];
2490
- for (const assignee of parsed.assignees) {
2153
+ for (const assignee of assignees) {
2491
2154
  if (assignee.toLowerCase() === "me") {
2492
2155
  try {
2493
- const currentUser = await linearService.getCurrentUser();
2156
+ const currentUser = await linearService.getCurrentUser(accountId);
2494
2157
  processedAssignees.push(currentUser.email);
2495
2158
  } catch {
2496
2159
  logger9.warn('Could not resolve "me" to current user');
@@ -2503,10 +2166,11 @@ var searchIssuesAction = {
2503
2166
  filters.assignee = processedAssignees;
2504
2167
  }
2505
2168
  }
2506
- if (parsed.hasAssignee === false) {
2169
+ if (getBooleanValue(parsed.hasAssignee) === false) {
2507
2170
  filters.query = filters.query ? `${filters.query} unassigned` : "unassigned";
2508
2171
  }
2509
- if (parsed.priorities && parsed.priorities.length > 0) {
2172
+ const parsedPriorities = getStringArrayValue(parsed.priorities);
2173
+ if (parsedPriorities && parsedPriorities.length > 0) {
2510
2174
  const priorityMap = {
2511
2175
  urgent: 1,
2512
2176
  high: 2,
@@ -2517,16 +2181,18 @@ var searchIssuesAction = {
2517
2181
  "3": 3,
2518
2182
  "4": 4
2519
2183
  };
2520
- const priorities = parsed.priorities.map((p) => priorityMap[p.toLowerCase()]).filter(Boolean);
2184
+ const priorities = parsedPriorities.map((p) => priorityMap[p.toLowerCase()]).filter(Boolean);
2521
2185
  if (priorities.length > 0) {
2522
2186
  filters.priority = priorities;
2523
2187
  }
2524
2188
  }
2525
- if (parsed.teams && parsed.teams.length > 0) {
2526
- filters.team = parsed.teams[0];
2189
+ const teams = getStringArrayValue(parsed.teams);
2190
+ if (teams && teams.length > 0) {
2191
+ filters.team = teams[0];
2527
2192
  }
2528
- if (parsed.labels && parsed.labels.length > 0) {
2529
- filters.label = parsed.labels;
2193
+ const labels = getStringArrayValue(parsed.labels);
2194
+ if (labels && labels.length > 0) {
2195
+ filters.label = labels;
2530
2196
  }
2531
2197
  Object.keys(filters).forEach((key) => {
2532
2198
  if (filters[key] === undefined) {
@@ -2540,7 +2206,7 @@ var searchIssuesAction = {
2540
2206
  }
2541
2207
  }
2542
2208
  if (!filters.team) {
2543
- const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
2209
+ const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
2544
2210
  if (defaultTeamKey) {
2545
2211
  const searchingAllIssues = content.toLowerCase().includes("all") && (content.toLowerCase().includes("issue") || content.toLowerCase().includes("bug") || content.toLowerCase().includes("task"));
2546
2212
  if (!searchingAllIssues) {
@@ -2550,7 +2216,7 @@ var searchIssuesAction = {
2550
2216
  }
2551
2217
  }
2552
2218
  filters.limit = params?.limit ?? filters.limit ?? 10;
2553
- const issues = await linearService.searchIssues(filters);
2219
+ const issues = await linearService.searchIssues(filters, accountId);
2554
2220
  if (issues.length === 0) {
2555
2221
  const noResultsMessage = "No issues found matching your search criteria.";
2556
2222
  await callback?.({
@@ -2563,7 +2229,8 @@ var searchIssuesAction = {
2563
2229
  data: {
2564
2230
  issues: [],
2565
2231
  filters: filters ? { ...filters } : undefined,
2566
- count: 0
2232
+ count: 0,
2233
+ accountId
2567
2234
  }
2568
2235
  };
2569
2236
  }
@@ -2607,12 +2274,209 @@ ${issueText}`;
2607
2274
  };
2608
2275
  })),
2609
2276
  filters: filters ? { ...filters } : undefined,
2610
- count: issues.length
2277
+ count: issues.length,
2278
+ accountId
2279
+ }
2280
+ };
2281
+ } catch (error) {
2282
+ logger9.error("Failed to search issues:", error);
2283
+ const errorMessage = `❌ Failed to search issues: ${error instanceof Error ? error.message : "Unknown error"}`;
2284
+ await callback?.({
2285
+ text: errorMessage,
2286
+ source: message.content.source
2287
+ });
2288
+ return {
2289
+ text: errorMessage,
2290
+ success: false
2291
+ };
2292
+ }
2293
+ }
2294
+ };
2295
+
2296
+ // src/actions/updateComment.ts
2297
+ import {
2298
+ logger as logger10
2299
+ } from "@elizaos/core";
2300
+ async function handleUpdateComment(runtime, message, _state, _options, callback) {
2301
+ try {
2302
+ const linearService = runtime.getService("linear");
2303
+ if (!linearService) {
2304
+ throw new Error("Linear service not available");
2305
+ }
2306
+ const accountId = getLinearAccountId(runtime, _options);
2307
+ const params = _options?.parameters;
2308
+ const commentId = params?.commentId?.trim() ?? "";
2309
+ const body = params?.body?.trim() ?? "";
2310
+ if (!commentId || !body) {
2311
+ const errorMessage = "Please provide both commentId and body to update a comment.";
2312
+ await callback?.({ text: errorMessage, source: message.content.source });
2313
+ return { text: errorMessage, success: false };
2314
+ }
2315
+ const comment = await linearService.updateComment(commentId, body, accountId);
2316
+ const successMessage = `Updated comment ${commentId}.`;
2317
+ await callback?.({ text: successMessage, source: message.content.source });
2318
+ return {
2319
+ text: successMessage,
2320
+ success: true,
2321
+ data: { commentId: comment.id, accountId }
2322
+ };
2323
+ } catch (error) {
2324
+ logger10.error("Failed to update comment:", error);
2325
+ const errorMessage = `Failed to update comment: ${error instanceof Error ? error.message : "Unknown error"}`;
2326
+ await callback?.({ text: errorMessage, source: message.content.source });
2327
+ return { text: errorMessage, success: false };
2328
+ }
2329
+ }
2330
+
2331
+ // src/actions/updateIssue.ts
2332
+ import {
2333
+ logger as logger11,
2334
+ ModelType as ModelType7
2335
+ } from "@elizaos/core";
2336
+ var LINEAR_MODEL_TIMEOUT_MS2 = 15000;
2337
+ var LINEAR_LOOKUP_LIMIT = 100;
2338
+ async function handleUpdateIssue(runtime, message, _state, _options, callback) {
2339
+ try {
2340
+ const linearService = runtime.getService("linear");
2341
+ if (!linearService) {
2342
+ throw new Error("Linear service not available");
2343
+ }
2344
+ const accountId = getLinearAccountId(runtime, _options);
2345
+ const content = message.content.text;
2346
+ if (!content) {
2347
+ const errorMessage = "Please provide update instructions for the issue.";
2348
+ await callback?.({
2349
+ text: errorMessage,
2350
+ source: message.content.source
2351
+ });
2352
+ return {
2353
+ text: errorMessage,
2354
+ success: false
2355
+ };
2356
+ }
2357
+ const prompt = updateIssueTemplate.replace("{{userMessage}}", content);
2358
+ const response = await Promise.race([
2359
+ runtime.useModel(ModelType7.TEXT_LARGE, {
2360
+ prompt
2361
+ }),
2362
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Linear update extraction timeout")), LINEAR_MODEL_TIMEOUT_MS2))
2363
+ ]);
2364
+ if (!response) {
2365
+ throw new Error("Failed to extract update information");
2366
+ }
2367
+ let issueId;
2368
+ const updates = {};
2369
+ try {
2370
+ const parsed = parseLinearPromptResponse(response);
2371
+ if (Object.keys(parsed).length === 0) {
2372
+ throw new Error("No fields found in model response");
2373
+ }
2374
+ issueId = getStringValue(parsed.issueId) ?? "";
2375
+ if (!issueId) {
2376
+ throw new Error("Issue ID not found in parsed response");
2377
+ }
2378
+ const parsedUpdates = getRecordValue(parsed.updates) ?? {};
2379
+ const title = getStringValue(parsedUpdates.title);
2380
+ if (title) {
2381
+ updates.title = title;
2382
+ }
2383
+ const description = getStringValue(parsedUpdates.description);
2384
+ if (description) {
2385
+ updates.description = description;
2386
+ }
2387
+ const priority = getPriorityNumberValue(parsedUpdates.priority);
2388
+ if (priority) {
2389
+ updates.priority = priority;
2390
+ }
2391
+ const teamKey = getStringValue(parsedUpdates.teamKey);
2392
+ if (teamKey) {
2393
+ const teams = await linearService.getTeams(accountId);
2394
+ const team = teams.slice(0, LINEAR_LOOKUP_LIMIT).find((t) => t.key.toLowerCase() === teamKey.toLowerCase());
2395
+ if (team) {
2396
+ updates.teamId = team.id;
2397
+ logger11.info(`Moving issue to team: ${team.name} (${team.key})`);
2398
+ } else {
2399
+ logger11.warn(`Team with key ${teamKey} not found`);
2611
2400
  }
2612
- };
2613
- } catch (error) {
2614
- logger9.error("Failed to search issues:", error);
2615
- const errorMessage = `❌ Failed to search issues: ${error instanceof Error ? error.message : "Unknown error"}`;
2401
+ }
2402
+ const assignee = getStringValue(parsedUpdates.assignee);
2403
+ if (assignee) {
2404
+ const cleanAssignee = assignee.replace(/^@/, "");
2405
+ const users = await linearService.getUsers(accountId);
2406
+ const user = users.slice(0, LINEAR_LOOKUP_LIMIT).find((u) => u.email === cleanAssignee || u.name.toLowerCase().includes(cleanAssignee.toLowerCase()));
2407
+ if (user) {
2408
+ updates.assigneeId = user.id;
2409
+ } else {
2410
+ logger11.warn(`User ${cleanAssignee} not found`);
2411
+ }
2412
+ }
2413
+ const status = getStringValue(parsedUpdates.status);
2414
+ if (status) {
2415
+ const issue = await linearService.getIssue(issueId, accountId);
2416
+ const issueTeam = await issue.team;
2417
+ const teamId = updates.teamId || issueTeam?.id;
2418
+ if (!teamId) {
2419
+ logger11.warn("Could not determine team for status update");
2420
+ } else {
2421
+ const states = await linearService.getWorkflowStates(teamId, accountId);
2422
+ const state = states.slice(0, LINEAR_LOOKUP_LIMIT).find((s) => s.name.toLowerCase() === status.toLowerCase() || s.type.toLowerCase() === status.toLowerCase());
2423
+ if (state) {
2424
+ updates.stateId = state.id;
2425
+ logger11.info(`Changing status to: ${state.name}`);
2426
+ } else {
2427
+ logger11.warn(`Status ${status} not found for team`);
2428
+ }
2429
+ }
2430
+ }
2431
+ const parsedLabels = getStringArrayValue(parsedUpdates.labels);
2432
+ if (parsedLabels !== undefined) {
2433
+ const teamId = updates.teamId;
2434
+ const labels = await linearService.getLabels(teamId, accountId);
2435
+ const labelIds = [];
2436
+ for (const labelName of parsedLabels.slice(0, LINEAR_LOOKUP_LIMIT)) {
2437
+ const label = labels.slice(0, LINEAR_LOOKUP_LIMIT).find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2438
+ if (label) {
2439
+ labelIds.push(label.id);
2440
+ }
2441
+ }
2442
+ updates.labelIds = labelIds;
2443
+ }
2444
+ } catch (parseError) {
2445
+ logger11.warn("Failed to parse LLM response, falling back to regex parsing:", parseError);
2446
+ const issueMatch = content.match(/(\w+-\d+)/);
2447
+ if (!issueMatch) {
2448
+ const errorMessage = "Please specify an issue ID (e.g., ENG-123) to update.";
2449
+ await callback?.({
2450
+ text: errorMessage,
2451
+ source: message.content.source
2452
+ });
2453
+ return {
2454
+ text: errorMessage,
2455
+ success: false
2456
+ };
2457
+ }
2458
+ issueId = issueMatch[1];
2459
+ const titleMatch = content.match(/title to ["'](.+?)["']/i);
2460
+ if (titleMatch) {
2461
+ updates.title = titleMatch[1];
2462
+ }
2463
+ const priorityMatch = content.match(/priority (?:to |as )?(\w+)/i);
2464
+ if (priorityMatch) {
2465
+ const priorityMap = {
2466
+ urgent: 1,
2467
+ high: 2,
2468
+ normal: 3,
2469
+ medium: 3,
2470
+ low: 4
2471
+ };
2472
+ const priority = priorityMap[priorityMatch[1].toLowerCase()];
2473
+ if (priority) {
2474
+ updates.priority = priority;
2475
+ }
2476
+ }
2477
+ }
2478
+ if (Object.keys(updates).length === 0) {
2479
+ const errorMessage = `No valid updates found. Please specify what to update (e.g., "Update issue ENG-123 title to 'New Title'")`;
2616
2480
  await callback?.({
2617
2481
  text: errorMessage,
2618
2482
  source: message.content.source
@@ -2622,331 +2486,484 @@ ${issueText}`;
2622
2486
  success: false
2623
2487
  };
2624
2488
  }
2489
+ const updatedIssue = await linearService.updateIssue(issueId, updates, accountId);
2490
+ const updateSummary = [];
2491
+ if (updates.title)
2492
+ updateSummary.push(`title: "${updates.title}"`);
2493
+ if (updates.priority)
2494
+ updateSummary.push(`priority: ${["", "urgent", "high", "normal", "low"][updates.priority]}`);
2495
+ if (updates.teamId)
2496
+ updateSummary.push(`moved to team`);
2497
+ if (updates.assigneeId)
2498
+ updateSummary.push(`assigned to user`);
2499
+ if (updates.stateId)
2500
+ updateSummary.push(`status changed`);
2501
+ if (updates.labelIds)
2502
+ updateSummary.push(`labels updated`);
2503
+ const successMessage = `✅ Updated issue ${updatedIssue.identifier}: ${updateSummary.join(", ")}
2504
+
2505
+ View it at: ${updatedIssue.url}`;
2506
+ await callback?.({
2507
+ text: successMessage,
2508
+ source: message.content.source
2509
+ });
2510
+ return {
2511
+ text: `Updated issue ${updatedIssue.identifier}: ${updateSummary.join(", ")}`,
2512
+ success: true,
2513
+ data: {
2514
+ issueId: updatedIssue.id,
2515
+ identifier: updatedIssue.identifier,
2516
+ updates: updates ? Object.fromEntries(Object.entries(updates).map(([key, value]) => [
2517
+ key,
2518
+ value instanceof Date ? value.toISOString() : value
2519
+ ])) : undefined,
2520
+ url: updatedIssue.url,
2521
+ accountId
2522
+ }
2523
+ };
2524
+ } catch (error) {
2525
+ logger11.error("Failed to update issue:", error);
2526
+ const errorMessage = `❌ Failed to update issue: ${error instanceof Error ? error.message : "Unknown error"}`;
2527
+ await callback?.({
2528
+ text: errorMessage,
2529
+ source: message.content.source
2530
+ });
2531
+ return {
2532
+ text: errorMessage,
2533
+ success: false
2534
+ };
2625
2535
  }
2626
- };
2536
+ }
2627
2537
 
2628
- // src/actions/updateIssue.ts
2629
- import {
2630
- logger as logger10,
2631
- ModelType as ModelType9
2632
- } from "@elizaos/core";
2633
- var updateIssueAction = {
2634
- name: "UPDATE_LINEAR_ISSUE",
2635
- description: "Update an existing Linear issue",
2538
+ // src/actions/linear.ts
2539
+ var LINEAR_CONTEXT = "linear";
2540
+ var ALL_OPS = [
2541
+ "create_issue",
2542
+ "get_issue",
2543
+ "update_issue",
2544
+ "delete_issue",
2545
+ "create_comment",
2546
+ "update_comment",
2547
+ "delete_comment",
2548
+ "list_comments",
2549
+ "get_activity",
2550
+ "clear_activity",
2551
+ "search_issues"
2552
+ ];
2553
+ var ROUTES = [
2554
+ {
2555
+ op: "delete_issue",
2556
+ action: deleteIssueAction,
2557
+ match: /\b(delete|archive|remove|close)\b.*\b(issue|bug|task|ticket|[a-z]+-\d+)\b/i
2558
+ },
2559
+ {
2560
+ op: "update_issue",
2561
+ run: handleUpdateIssue,
2562
+ match: /\b(update|edit|modify|move|change|assign|reassign|priority|status|label)\b.*\b(issue|bug|task|ticket|[a-z]+-\d+)\b/i
2563
+ },
2564
+ {
2565
+ op: "create_issue",
2566
+ action: createIssueAction,
2567
+ match: /\b(create|new|add|file|open)\b.*\b(issue|bug|task|ticket|linear)\b|\b(issue|bug|task|ticket)\b.*\b(create|new|add|file|open)\b/i
2568
+ },
2569
+ {
2570
+ op: "create_comment",
2571
+ action: createCommentAction,
2572
+ match: /\b(comment|reply|note|tell)\b.*\b(issue|bug|task|ticket|[a-z]+-\d+)\b/i
2573
+ },
2574
+ {
2575
+ op: "update_comment",
2576
+ run: handleUpdateComment,
2577
+ match: /\b(update|edit|modify|change)\b.*\bcomment\b/i
2578
+ },
2579
+ {
2580
+ op: "delete_comment",
2581
+ action: deleteCommentAction,
2582
+ match: /\b(delete|remove|erase)\b.*\bcomment\b/i
2583
+ },
2584
+ {
2585
+ op: "list_comments",
2586
+ action: listCommentsAction,
2587
+ match: /\b(list|show|get|fetch|view)\b.*\bcomments?\b|\bcomments?\b.*\b(list|show|get|fetch)\b/i
2588
+ },
2589
+ {
2590
+ op: "clear_activity",
2591
+ action: clearActivityAction,
2592
+ match: /\b(clear|reset|delete)\b.*\b(activity|activity log)\b/i
2593
+ },
2594
+ {
2595
+ op: "get_activity",
2596
+ action: getActivityAction,
2597
+ match: /\b(activity|activity log|what happened|recent changes|audit)\b/i
2598
+ },
2599
+ {
2600
+ op: "search_issues",
2601
+ action: searchIssuesAction,
2602
+ match: /\b(search|find|query|list|show)\b.*\b(issues?|bugs?|tasks?|tickets?)\b|\b(open|closed|unassigned|assigned|high priority|blockers?)\b.*\b(issues?|bugs?|tasks?|tickets?)\b/i
2603
+ },
2604
+ {
2605
+ op: "get_issue",
2606
+ action: getIssueAction,
2607
+ match: /\b(show|get|view|check|details?|status|what'?s|find)\b.*\b(issue|bug|task|ticket|[a-z]+-\d+)\b|[a-z]+-\d+/i
2608
+ }
2609
+ ];
2610
+ function textOf(message) {
2611
+ return typeof message.content?.text === "string" ? message.content.text : "";
2612
+ }
2613
+ function readOptions(options) {
2614
+ const direct = options ?? {};
2615
+ const parameters = direct.parameters && typeof direct.parameters === "object" ? direct.parameters : {};
2616
+ return { ...direct, ...parameters };
2617
+ }
2618
+ function normalizeOp(value) {
2619
+ if (typeof value !== "string")
2620
+ return null;
2621
+ const trimmed = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
2622
+ return ALL_OPS.includes(trimmed) ? trimmed : null;
2623
+ }
2624
+ function selectRoute(message, options) {
2625
+ const opts = readOptions(options);
2626
+ const requested = normalizeOp(opts.action ?? opts.subaction ?? opts.op);
2627
+ if (requested) {
2628
+ const route = ROUTES.find((candidate) => candidate.op === requested);
2629
+ if (route)
2630
+ return route;
2631
+ }
2632
+ const text = textOf(message);
2633
+ return ROUTES.find((route) => route.match.test(text)) ?? null;
2634
+ }
2635
+ function hasLinearAccess(runtime) {
2636
+ return hasLinearAccountConfig(runtime);
2637
+ }
2638
+ var linearAction = {
2639
+ name: "LINEAR",
2640
+ description: "Manage Linear issues, comments, and activity. Operations: create_issue, get_issue, update_issue, delete_issue, create_comment, update_comment, delete_comment, list_comments, get_activity, clear_activity, search_issues. The op is inferred from the message text when not explicitly provided.",
2641
+ descriptionCompressed: "Linear: create/get/update/delete issue, create/update/delete/list comment, search issues, get/clear activity.",
2636
2642
  similes: [
2637
- "update-linear-issue",
2638
- "edit-linear-issue",
2639
- "modify-linear-issue",
2640
- "move-linear-issue",
2641
- "change-linear-issue"
2643
+ "LINEAR_ISSUE",
2644
+ "LINEAR_ISSUES",
2645
+ "LINEAR_COMMENT",
2646
+ "LINEAR_COMMENTS",
2647
+ "LINEAR_WORKFLOW",
2648
+ "LINEAR_ACTIVITY",
2649
+ "LINEAR_SEARCH",
2650
+ "CREATE_LINEAR_ISSUE",
2651
+ "GET_LINEAR_ISSUE",
2652
+ "UPDATE_LINEAR_ISSUE",
2653
+ "DELETE_LINEAR_ISSUE",
2654
+ "MANAGE_LINEAR_ISSUE",
2655
+ "MANAGE_LINEAR_ISSUES",
2656
+ "CREATE_LINEAR_COMMENT",
2657
+ "COMMENT_LINEAR_ISSUE",
2658
+ "UPDATE_LINEAR_COMMENT",
2659
+ "DELETE_LINEAR_COMMENT",
2660
+ "LIST_LINEAR_COMMENTS",
2661
+ "GET_LINEAR_ACTIVITY",
2662
+ "CLEAR_LINEAR_ACTIVITY",
2663
+ "SEARCH_LINEAR_ISSUES",
2664
+ "LINEAR_WORKFLOW_SEARCH"
2642
2665
  ],
2666
+ contexts: ["general", "automation", "knowledge", LINEAR_CONTEXT],
2667
+ contextGate: { anyOf: ["general", "automation", "knowledge", LINEAR_CONTEXT] },
2668
+ roleGate: { minRole: "USER" },
2669
+ parameters: [
2670
+ {
2671
+ name: "action",
2672
+ description: "Operation to perform. One of: create_issue, get_issue, update_issue, delete_issue, create_comment, update_comment, delete_comment, list_comments, get_activity, clear_activity, search_issues. Inferred from message text when omitted.",
2673
+ required: false,
2674
+ schema: { type: "string", enum: [...ALL_OPS] }
2675
+ },
2676
+ linearAccountIdParameter
2677
+ ],
2678
+ validate: async (runtime) => {
2679
+ if (!hasLinearAccess(runtime))
2680
+ return false;
2681
+ return true;
2682
+ },
2683
+ handler: async (runtime, message, state, options, callback) => {
2684
+ const route = selectRoute(message, options);
2685
+ if (!route) {
2686
+ const ops = ALL_OPS.join(", ");
2687
+ const text = `LINEAR could not determine the operation. Specify one of: ${ops}.`;
2688
+ await callback?.({ text, source: message.content?.source });
2689
+ return {
2690
+ success: false,
2691
+ text,
2692
+ values: { error: "MISSING" },
2693
+ data: { actionName: "LINEAR", availableOps: ops }
2694
+ };
2695
+ }
2696
+ const dispatch = route.run ?? route.action?.handler?.bind(route.action);
2697
+ const result = dispatch ? await dispatch(runtime, message, state, options, callback) ?? { success: true } : { success: true };
2698
+ return {
2699
+ ...result,
2700
+ data: {
2701
+ ...typeof result.data === "object" && result.data ? result.data : {},
2702
+ actionName: "LINEAR",
2703
+ routedActionName: route.action?.name ?? route.op,
2704
+ op: route.op
2705
+ }
2706
+ };
2707
+ },
2643
2708
  examples: [
2644
2709
  [
2710
+ { name: "{{user1}}", content: { text: "Create a Linear issue for the mobile login bug" } },
2645
2711
  {
2646
- name: "User",
2647
- content: {
2648
- text: 'Update issue ENG-123 title to "Fix login button on all devices"'
2649
- }
2650
- },
2651
- {
2652
- name: "Assistant",
2712
+ name: "{{agentName}}",
2653
2713
  content: {
2654
- text: "I'll update the title of issue ENG-123 for you.",
2655
- actions: ["UPDATE_LINEAR_ISSUE"]
2714
+ text: "I'll create that Linear issue.",
2715
+ actions: ["LINEAR"]
2656
2716
  }
2657
2717
  }
2658
2718
  ],
2659
2719
  [
2720
+ { name: "{{user1}}", content: { text: "Comment on ENG-123 that QA can retest it" } },
2660
2721
  {
2661
- name: "User",
2662
- content: {
2663
- text: "Move issue COM2-7 to the ELIZA team"
2664
- }
2665
- },
2666
- {
2667
- name: "Assistant",
2668
- content: {
2669
- text: "I'll move issue COM2-7 to the ELIZA team.",
2670
- actions: ["UPDATE_LINEAR_ISSUE"]
2671
- }
2722
+ name: "{{agentName}}",
2723
+ content: { text: "I'll add that comment to ENG-123.", actions: ["LINEAR"] }
2672
2724
  }
2673
2725
  ],
2674
2726
  [
2727
+ { name: "{{user1}}", content: { text: "Search open Linear bugs for the backend team" } },
2675
2728
  {
2676
- name: "User",
2677
- content: {
2678
- text: "Change the priority of BUG-456 to high and assign to john@example.com"
2679
- }
2680
- },
2729
+ name: "{{agentName}}",
2730
+ content: { text: "I'll search Linear issues.", actions: ["LINEAR"] }
2731
+ }
2732
+ ],
2733
+ [
2734
+ { name: "{{user1}}", content: { text: "What's the status of ENG-456?" } },
2681
2735
  {
2682
- name: "Assistant",
2683
- content: {
2684
- text: "I'll change the priority of BUG-456 to high and assign it to john@example.com.",
2685
- actions: ["UPDATE_LINEAR_ISSUE"]
2686
- }
2736
+ name: "{{agentName}}",
2737
+ content: { text: "Looking up ENG-456.", actions: ["LINEAR"] }
2687
2738
  }
2688
2739
  ]
2689
- ],
2690
- validate: async (runtime, message, state, options) => {
2691
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
2692
- const __avText = __avTextRaw.toLowerCase();
2693
- const __avKeywords = ["update", "linear", "issue"];
2694
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((word) => word.length > 0 && __avText.includes(word));
2695
- const __avRegex = /\b(?:update|linear|issue)\b/i;
2696
- const __avRegexOk = __avRegex.test(__avText);
2697
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
2698
- const __avExpectedSource = "";
2699
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService || runtime?.getSetting);
2700
- const __avOptions = options && typeof options === "object" ? options : {};
2701
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
2702
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
2703
- return false;
2740
+ ]
2741
+ };
2742
+
2743
+ // src/connector-account-provider.ts
2744
+ import {
2745
+ logger as logger12
2746
+ } from "@elizaos/core";
2747
+ var LINEAR_PROVIDER_NAME = "linear";
2748
+ var LINEAR_AUTHORIZATION_ENDPOINT = "https://linear.app/oauth/authorize";
2749
+ var LINEAR_TOKEN_ENDPOINT = "https://api.linear.app/oauth/token";
2750
+ var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
2751
+ var DEFAULT_PURPOSES = ["admin"];
2752
+ function nonEmptyString2(value) {
2753
+ if (typeof value !== "string")
2754
+ return;
2755
+ const trimmed = value.trim();
2756
+ return trimmed.length > 0 ? trimmed : undefined;
2757
+ }
2758
+ function readSetting2(runtime, key) {
2759
+ return nonEmptyString2(runtime.getSetting?.(key));
2760
+ }
2761
+ function readClientConfig(runtime) {
2762
+ const clientId = readSetting2(runtime, "LINEAR_OAUTH_CLIENT_ID");
2763
+ const clientSecret = readSetting2(runtime, "LINEAR_OAUTH_CLIENT_SECRET");
2764
+ const redirectUri = readSetting2(runtime, "LINEAR_OAUTH_REDIRECT_URI");
2765
+ if (!clientId || !clientSecret || !redirectUri) {
2766
+ throw new Error("Linear OAuth requires LINEAR_OAUTH_CLIENT_ID, LINEAR_OAUTH_CLIENT_SECRET, and LINEAR_OAUTH_REDIRECT_URI to be configured.");
2767
+ }
2768
+ return { clientId, clientSecret, redirectUri };
2769
+ }
2770
+ function parseScopes(value) {
2771
+ if (!value)
2772
+ return [];
2773
+ return value.split(/[,\s]+/).map((scope) => scope.trim()).filter(Boolean);
2774
+ }
2775
+ async function exchangeCodeForToken(args) {
2776
+ const response = await fetch(LINEAR_TOKEN_ENDPOINT, {
2777
+ method: "POST",
2778
+ headers: {
2779
+ "Content-Type": "application/x-www-form-urlencoded",
2780
+ Accept: "application/json"
2781
+ },
2782
+ body: new URLSearchParams({
2783
+ client_id: args.clientId,
2784
+ client_secret: args.clientSecret,
2785
+ code: args.code,
2786
+ redirect_uri: args.redirectUri,
2787
+ grant_type: "authorization_code"
2788
+ }).toString()
2789
+ });
2790
+ if (!response.ok) {
2791
+ const body = await response.text();
2792
+ throw new Error(`Linear token exchange failed with ${response.status}: ${body}`);
2793
+ }
2794
+ const parsed = await response.json();
2795
+ if (parsed.error) {
2796
+ throw new Error(`Linear token exchange returned error ${parsed.error}: ${parsed.error_description ?? "no description"}`);
2797
+ }
2798
+ if (!parsed.access_token) {
2799
+ throw new Error("Linear token exchange returned no access_token.");
2800
+ }
2801
+ return parsed;
2802
+ }
2803
+ async function fetchLinearViewer(accessToken) {
2804
+ const response = await fetch(LINEAR_GRAPHQL_ENDPOINT, {
2805
+ method: "POST",
2806
+ headers: {
2807
+ Authorization: `Bearer ${accessToken}`,
2808
+ "Content-Type": "application/json"
2809
+ },
2810
+ body: JSON.stringify({
2811
+ query: "{ viewer { id name email organization { id name urlKey } } }"
2812
+ })
2813
+ });
2814
+ if (!response.ok) {
2815
+ throw new Error(`Linear viewer query failed with ${response.status}`);
2816
+ }
2817
+ return await response.json();
2818
+ }
2819
+ function synthesizeEnvAccounts(runtime) {
2820
+ const now = Date.now();
2821
+ return readLinearAccounts(runtime).map((account) => ({
2822
+ id: account.accountId,
2823
+ provider: LINEAR_PROVIDER_NAME,
2824
+ label: account.label ?? `Linear (${account.accountId})`,
2825
+ role: "OWNER",
2826
+ purpose: DEFAULT_PURPOSES,
2827
+ accessGate: "open",
2828
+ status: "connected",
2829
+ externalId: account.workspaceId,
2830
+ displayHandle: account.workspaceId ?? account.accountId,
2831
+ createdAt: now,
2832
+ updatedAt: now,
2833
+ metadata: {
2834
+ authMethod: "api_key",
2835
+ source: "env",
2836
+ defaultTeamKey: account.defaultTeamKey ?? null
2704
2837
  }
2705
- const __avLegacyValidate = async (runtime2, message2, state2, options2) => {
2706
- const __avTextRaw2 = typeof message2?.content?.text === "string" ? message2.content.text : "";
2707
- const __avText2 = __avTextRaw2.toLowerCase();
2708
- const __avKeywords2 = ["update", "linear", "issue"];
2709
- const __avKeywordOk2 = __avKeywords2.length > 0 && __avKeywords2.some((word) => word.length > 0 && __avText2.includes(word));
2710
- const __avRegex2 = /\b(?:update|linear|issue)\b/i;
2711
- const __avRegexOk2 = __avRegex2.test(__avText2);
2712
- const __avSource2 = String(message2?.content?.source ?? message2?.source ?? "");
2713
- const __avExpectedSource2 = "";
2714
- const __avSourceOk2 = __avExpectedSource2 ? __avSource2 === __avExpectedSource2 : Boolean(__avSource2 || state2 || runtime2?.agentId || runtime2?.getService || runtime2?.getSetting);
2715
- const __avOptions2 = options2 && typeof options2 === "object" ? options2 : {};
2716
- const __avInputOk2 = __avText2.trim().length > 0 || Object.keys(__avOptions2).length > 0 || Boolean(message2?.content && typeof message2.content === "object");
2717
- if (!(__avKeywordOk2 && __avRegexOk2 && __avSourceOk2 && __avInputOk2)) {
2718
- return false;
2719
- }
2720
- const __avLegacyValidate2 = async (runtime3, message3, state3, options3) => {
2721
- const __avTextRaw3 = typeof message3?.content?.text === "string" ? message3.content.text : "";
2722
- const __avText3 = __avTextRaw3.toLowerCase();
2723
- const __avKeywords3 = ["update", "linear", "issue"];
2724
- const __avKeywordOk3 = __avKeywords3.length > 0 && __avKeywords3.some((kw) => kw.length > 0 && __avText3.includes(kw));
2725
- const __avRegex3 = /\b(?:update|linear|issue)\b/i;
2726
- const __avRegexOk3 = __avRegex3.test(__avText3);
2727
- const __avSource3 = String(message3?.content?.source ?? message3?.source ?? "");
2728
- const __avExpectedSource3 = "";
2729
- const __avSourceOk3 = __avExpectedSource3 ? __avSource3 === __avExpectedSource3 : Boolean(__avSource3 || state3 || runtime3?.agentId || runtime3?.getService);
2730
- const __avOptions3 = options3 && typeof options3 === "object" ? options3 : {};
2731
- const __avInputOk3 = __avText3.trim().length > 0 || Object.keys(__avOptions3).length > 0 || Boolean(message3?.content && typeof message3.content === "object");
2732
- if (!(__avKeywordOk3 && __avRegexOk3 && __avSourceOk3 && __avInputOk3)) {
2733
- return false;
2734
- }
2735
- const __avLegacyValidate3 = async (runtime4, _message, _state) => {
2736
- const apiKey = runtime4.getSetting("LINEAR_API_KEY");
2737
- return !!apiKey;
2738
- };
2739
- try {
2740
- return Boolean(await __avLegacyValidate3(runtime3, message3, state3, options3));
2741
- } catch {
2742
- return false;
2743
- }
2838
+ }));
2839
+ }
2840
+ function createLinearConnectorAccountProvider(runtime) {
2841
+ return {
2842
+ provider: LINEAR_PROVIDER_NAME,
2843
+ label: "Linear",
2844
+ listAccounts: async (manager) => {
2845
+ const stored = await manager.getStorage().listAccounts(LINEAR_PROVIDER_NAME);
2846
+ if (stored.length > 0)
2847
+ return stored;
2848
+ return synthesizeEnvAccounts(runtime);
2849
+ },
2850
+ createAccount: async (input, _manager) => {
2851
+ return {
2852
+ ...input,
2853
+ provider: LINEAR_PROVIDER_NAME,
2854
+ role: input.role ?? "OWNER",
2855
+ purpose: input.purpose ?? DEFAULT_PURPOSES,
2856
+ accessGate: input.accessGate ?? "open",
2857
+ status: input.status ?? "pending"
2744
2858
  };
2745
- try {
2746
- return Boolean(await __avLegacyValidate2(runtime2, message2, state2, options2));
2747
- } catch {
2748
- return false;
2749
- }
2750
- };
2751
- try {
2752
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
2753
- } catch {
2754
- return false;
2755
- }
2756
- },
2757
- async handler(runtime, message, _state, _options, callback) {
2758
- try {
2759
- const linearService = runtime.getService("linear");
2760
- if (!linearService) {
2761
- throw new Error("Linear service not available");
2762
- }
2763
- const content = message.content.text;
2764
- if (!content) {
2765
- const errorMessage = "Please provide update instructions for the issue.";
2766
- await callback?.({
2767
- text: errorMessage,
2768
- source: message.content.source
2769
- });
2770
- return {
2771
- text: errorMessage,
2772
- success: false
2773
- };
2774
- }
2775
- const prompt = updateIssueTemplate.replace("{{userMessage}}", content);
2776
- const response = await runtime.useModel(ModelType9.TEXT_LARGE, {
2777
- prompt
2778
- });
2779
- if (!response) {
2780
- throw new Error("Failed to extract update information");
2781
- }
2782
- let issueId;
2783
- const updates = {};
2784
- try {
2785
- const cleanedResponse = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
2786
- const parsed = JSON.parse(cleanedResponse);
2787
- issueId = parsed.issueId;
2788
- if (!issueId) {
2789
- throw new Error("Issue ID not found in parsed response");
2790
- }
2791
- if (parsed.updates?.title) {
2792
- updates.title = parsed.updates.title;
2793
- }
2794
- if (parsed.updates?.description) {
2795
- updates.description = parsed.updates.description;
2796
- }
2797
- if (parsed.updates?.priority) {
2798
- updates.priority = Number(parsed.updates.priority);
2799
- }
2800
- if (parsed.updates?.teamKey) {
2801
- const teams = await linearService.getTeams();
2802
- const team = teams.find((t) => t.key.toLowerCase() === parsed.updates.teamKey.toLowerCase());
2803
- if (team) {
2804
- updates.teamId = team.id;
2805
- logger10.info(`Moving issue to team: ${team.name} (${team.key})`);
2806
- } else {
2807
- logger10.warn(`Team with key ${parsed.updates.teamKey} not found`);
2808
- }
2809
- }
2810
- if (parsed.updates?.assignee) {
2811
- const cleanAssignee = parsed.updates.assignee.replace(/^@/, "");
2812
- const users = await linearService.getUsers();
2813
- const user = users.find((u) => u.email === cleanAssignee || u.name.toLowerCase().includes(cleanAssignee.toLowerCase()));
2814
- if (user) {
2815
- updates.assigneeId = user.id;
2816
- } else {
2817
- logger10.warn(`User ${cleanAssignee} not found`);
2818
- }
2819
- }
2820
- if (parsed.updates?.status) {
2821
- const issue = await linearService.getIssue(issueId);
2822
- const issueTeam = await issue.team;
2823
- const teamId = updates.teamId || issueTeam?.id;
2824
- if (!teamId) {
2825
- logger10.warn("Could not determine team for status update");
2826
- } else {
2827
- const states = await linearService.getWorkflowStates(teamId);
2828
- const state = states.find((s) => s.name.toLowerCase() === parsed.updates.status.toLowerCase() || s.type.toLowerCase() === parsed.updates.status.toLowerCase());
2829
- if (state) {
2830
- updates.stateId = state.id;
2831
- logger10.info(`Changing status to: ${state.name}`);
2832
- } else {
2833
- logger10.warn(`Status ${parsed.updates.status} not found for team`);
2834
- }
2835
- }
2836
- }
2837
- if (parsed.updates?.labels && Array.isArray(parsed.updates.labels)) {
2838
- const teamId = updates.teamId;
2839
- const labels = await linearService.getLabels(teamId);
2840
- const labelIds = [];
2841
- for (const labelName of parsed.updates.labels) {
2842
- if (labelName) {
2843
- const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2844
- if (label) {
2845
- labelIds.push(label.id);
2846
- }
2847
- }
2848
- }
2849
- updates.labelIds = labelIds;
2850
- }
2851
- } catch (parseError) {
2852
- logger10.warn("Failed to parse LLM response, falling back to regex parsing:", parseError);
2853
- const issueMatch = content.match(/(\w+-\d+)/);
2854
- if (!issueMatch) {
2855
- const errorMessage = "Please specify an issue ID (e.g., ENG-123) to update.";
2856
- await callback?.({
2857
- text: errorMessage,
2858
- source: message.content.source
2859
- });
2860
- return {
2861
- text: errorMessage,
2862
- success: false
2863
- };
2864
- }
2865
- issueId = issueMatch[1];
2866
- const titleMatch = content.match(/title to ["'](.+?)["']/i);
2867
- if (titleMatch) {
2868
- updates.title = titleMatch[1];
2869
- }
2870
- const priorityMatch = content.match(/priority (?:to |as )?(\w+)/i);
2871
- if (priorityMatch) {
2872
- const priorityMap = {
2873
- urgent: 1,
2874
- high: 2,
2875
- normal: 3,
2876
- medium: 3,
2877
- low: 4
2878
- };
2879
- const priority = priorityMap[priorityMatch[1].toLowerCase()];
2880
- if (priority) {
2881
- updates.priority = priority;
2882
- }
2883
- }
2884
- }
2885
- if (Object.keys(updates).length === 0) {
2886
- const errorMessage = `No valid updates found. Please specify what to update (e.g., "Update issue ENG-123 title to 'New Title'")`;
2887
- await callback?.({
2888
- text: errorMessage,
2889
- source: message.content.source
2890
- });
2891
- return {
2892
- text: errorMessage,
2893
- success: false
2894
- };
2895
- }
2896
- const updatedIssue = await linearService.updateIssue(issueId, updates);
2897
- const updateSummary = [];
2898
- if (updates.title)
2899
- updateSummary.push(`title: "${updates.title}"`);
2900
- if (updates.priority)
2901
- updateSummary.push(`priority: ${["", "urgent", "high", "normal", "low"][updates.priority]}`);
2902
- if (updates.teamId)
2903
- updateSummary.push(`moved to team`);
2904
- if (updates.assigneeId)
2905
- updateSummary.push(`assigned to user`);
2906
- if (updates.stateId)
2907
- updateSummary.push(`status changed`);
2908
- if (updates.labelIds)
2909
- updateSummary.push(`labels updated`);
2910
- const successMessage = `✅ Updated issue ${updatedIssue.identifier}: ${updateSummary.join(", ")}
2911
-
2912
- View it at: ${updatedIssue.url}`;
2913
- await callback?.({
2914
- text: successMessage,
2915
- source: message.content.source
2859
+ },
2860
+ patchAccount: async (_accountId, patch, _manager) => {
2861
+ return { ...patch, provider: LINEAR_PROVIDER_NAME };
2862
+ },
2863
+ deleteAccount: async (_accountId, _manager) => {},
2864
+ startOAuth: async (request, _manager) => {
2865
+ const config = readClientConfig(runtime);
2866
+ const redirectUri = request.redirectUri ?? config.redirectUri;
2867
+ const scopes = request.scopes && request.scopes.length > 0 ? request.scopes : ["read", "write", "issues:create", "comments:create"];
2868
+ const params = new URLSearchParams({
2869
+ client_id: config.clientId,
2870
+ redirect_uri: redirectUri,
2871
+ response_type: "code",
2872
+ scope: scopes.join(","),
2873
+ state: request.flow.state,
2874
+ prompt: "consent"
2916
2875
  });
2917
2876
  return {
2918
- text: `Updated issue ${updatedIssue.identifier}: ${updateSummary.join(", ")}`,
2919
- success: true,
2920
- data: {
2921
- issueId: updatedIssue.id,
2922
- identifier: updatedIssue.identifier,
2923
- updates: updates ? Object.fromEntries(Object.entries(updates).map(([key, value]) => [
2924
- key,
2925
- value instanceof Date ? value.toISOString() : value
2926
- ])) : undefined,
2927
- url: updatedIssue.url
2877
+ authUrl: `${LINEAR_AUTHORIZATION_ENDPOINT}?${params.toString()}`,
2878
+ metadata: {
2879
+ ...request.metadata,
2880
+ requestedScopes: scopes,
2881
+ redirectUri
2928
2882
  }
2929
2883
  };
2930
- } catch (error) {
2931
- logger10.error("Failed to update issue:", error);
2932
- const errorMessage = `❌ Failed to update issue: ${error instanceof Error ? error.message : "Unknown error"}`;
2933
- await callback?.({
2934
- text: errorMessage,
2935
- source: message.content.source
2884
+ },
2885
+ completeOAuth: async (request, _manager) => {
2886
+ const code = nonEmptyString2(request.code);
2887
+ if (!code) {
2888
+ throw new Error("Linear OAuth callback is missing an authorization code.");
2889
+ }
2890
+ const config = readClientConfig(runtime);
2891
+ const redirectUri = nonEmptyString2(request.flow.redirectUri) ?? config.redirectUri;
2892
+ const tokens = await exchangeCodeForToken({
2893
+ clientId: config.clientId,
2894
+ clientSecret: config.clientSecret,
2895
+ redirectUri,
2896
+ code
2936
2897
  });
2898
+ if (!tokens.access_token) {
2899
+ throw new Error("Linear token exchange returned no access_token.");
2900
+ }
2901
+ const viewerPayload = await fetchLinearViewer(tokens.access_token);
2902
+ const viewer = viewerPayload.data?.viewer;
2903
+ const organization = viewer?.organization;
2904
+ const workspaceId = nonEmptyString2(organization?.id);
2905
+ const workspaceHandle = nonEmptyString2(organization?.urlKey);
2906
+ const externalId = workspaceId ?? workspaceHandle;
2907
+ if (!externalId) {
2908
+ throw new Error("Linear viewer payload did not include an organization id or urlKey.");
2909
+ }
2910
+ const accountPatch = {
2911
+ provider: LINEAR_PROVIDER_NAME,
2912
+ role: "OWNER",
2913
+ purpose: DEFAULT_PURPOSES,
2914
+ accessGate: "open",
2915
+ status: "connected",
2916
+ externalId,
2917
+ displayHandle: workspaceHandle ?? externalId,
2918
+ label: nonEmptyString2(organization?.name) ?? nonEmptyString2(workspaceHandle) ?? "Linear",
2919
+ metadata: {
2920
+ authMethod: "oauth",
2921
+ workspaceId: workspaceId ?? null,
2922
+ workspaceHandle: workspaceHandle ?? null,
2923
+ workspaceName: nonEmptyString2(organization?.name) ?? null,
2924
+ viewerId: nonEmptyString2(viewer?.id) ?? null,
2925
+ viewerEmail: nonEmptyString2(viewer?.email) ?? null,
2926
+ viewerName: nonEmptyString2(viewer?.name) ?? null,
2927
+ tokenType: nonEmptyString2(tokens.token_type) ?? "bearer",
2928
+ grantedScopes: parseScopes(tokens.scope),
2929
+ hasRefreshToken: Boolean(tokens.refresh_token)
2930
+ }
2931
+ };
2932
+ logger12.info({
2933
+ src: "plugin:linear:connector",
2934
+ workspaceId: workspaceId ?? null,
2935
+ workspaceHandle: workspaceHandle ?? null
2936
+ }, "Linear OAuth completed");
2937
2937
  return {
2938
- text: errorMessage,
2939
- success: false
2938
+ account: accountPatch,
2939
+ flow: { status: "completed" }
2940
2940
  };
2941
2941
  }
2942
- }
2943
- };
2942
+ };
2943
+ }
2944
2944
 
2945
2945
  // src/providers/activity.ts
2946
+ function formatDetails(details) {
2947
+ if (details === null || details === undefined) {
2948
+ return "none";
2949
+ }
2950
+ if (Array.isArray(details)) {
2951
+ return details.map(formatDetails).join(", ");
2952
+ }
2953
+ if (typeof details !== "object") {
2954
+ return String(details);
2955
+ }
2956
+ return Object.entries(details).map(([key, value]) => `${key}: ${formatDetails(value)}`).join("; ");
2957
+ }
2946
2958
  var linearActivityProvider = {
2947
2959
  name: "LINEAR_ACTIVITY",
2948
2960
  description: "Provides context about recent Linear activity",
2961
+ descriptionCompressed: "provide context recent Linear activity",
2949
2962
  dynamic: true,
2963
+ contexts: ["automation", "connectors"],
2964
+ contextGate: { anyOf: ["automation", "connectors"] },
2965
+ cacheScope: "turn",
2966
+ roleGate: { minRole: "ADMIN" },
2950
2967
  get: async (runtime, _message, _state) => {
2951
2968
  try {
2952
2969
  const linearService = runtime.getService("linear");
@@ -2979,7 +2996,7 @@ ${activityList.join(`
2979
2996
  resource_id: item.resource_id,
2980
2997
  success: item.success,
2981
2998
  error: item.error,
2982
- details: JSON.stringify(item.details),
2999
+ details: formatDetails(item.details),
2983
3000
  timestamp: typeof item.timestamp === "string" ? item.timestamp : new Date(item.timestamp).toISOString()
2984
3001
  }))
2985
3002
  }
@@ -2996,7 +3013,12 @@ ${activityList.join(`
2996
3013
  var linearIssuesProvider = {
2997
3014
  name: "LINEAR_ISSUES",
2998
3015
  description: "Provides context about recent Linear issues",
3016
+ descriptionCompressed: "provide context recent Linear issue",
2999
3017
  dynamic: true,
3018
+ contexts: ["automation", "connectors"],
3019
+ contextGate: { anyOf: ["automation", "connectors"] },
3020
+ cacheScope: "turn",
3021
+ roleGate: { minRole: "ADMIN" },
3000
3022
  get: async (runtime, _message, _state) => {
3001
3023
  try {
3002
3024
  const linearService = runtime.getService("linear");
@@ -3040,7 +3062,12 @@ ${issuesList.join(`
3040
3062
  var linearProjectsProvider = {
3041
3063
  name: "LINEAR_PROJECTS",
3042
3064
  description: "Provides context about active Linear projects",
3065
+ descriptionCompressed: "provide context active Linear project",
3043
3066
  dynamic: true,
3067
+ contexts: ["automation", "connectors"],
3068
+ contextGate: { anyOf: ["automation", "connectors"] },
3069
+ cacheScope: "agent",
3070
+ roleGate: { minRole: "ADMIN" },
3044
3071
  get: async (runtime, _message, _state) => {
3045
3072
  try {
3046
3073
  const linearService = runtime.getService("linear");
@@ -3079,10 +3106,17 @@ ${projectsList.join(`
3079
3106
  };
3080
3107
 
3081
3108
  // src/providers/teams.ts
3109
+ var MAX_LINEAR_TEAMS = 20;
3110
+ var MAX_DESCRIPTION_CHARS = 180;
3082
3111
  var linearTeamsProvider = {
3083
3112
  name: "LINEAR_TEAMS",
3084
3113
  description: "Provides context about Linear teams",
3114
+ descriptionCompressed: "provide context Linear team",
3085
3115
  dynamic: true,
3116
+ contexts: ["automation", "connectors"],
3117
+ contextGate: { anyOf: ["automation", "connectors"] },
3118
+ cacheScope: "agent",
3119
+ roleGate: { minRole: "ADMIN" },
3086
3120
  get: async (runtime, _message, _state) => {
3087
3121
  try {
3088
3122
  const linearService = runtime.getService("linear");
@@ -3092,23 +3126,25 @@ var linearTeamsProvider = {
3092
3126
  };
3093
3127
  }
3094
3128
  const teams = await linearService.getTeams();
3129
+ const listedTeams = teams.slice(0, MAX_LINEAR_TEAMS);
3095
3130
  if (teams.length === 0) {
3096
3131
  return {
3097
3132
  text: "No Linear teams found"
3098
3133
  };
3099
3134
  }
3100
- const teamsList = teams.map((team) => `- ${team.name} (${team.key}): ${team.description || "No description"}`);
3135
+ const teamsList = listedTeams.map((team) => `- ${team.name} (${team.key}): ${(team.description || "No description").slice(0, MAX_DESCRIPTION_CHARS)}`);
3101
3136
  const text = `Linear Teams:
3102
3137
  ${teamsList.join(`
3103
3138
  `)}`;
3104
3139
  return {
3105
3140
  text,
3106
3141
  data: {
3107
- teams: teams.map((team) => ({
3142
+ teams: listedTeams.map((team) => ({
3108
3143
  id: team.id,
3109
3144
  name: team.name,
3110
3145
  key: team.key
3111
- }))
3146
+ })),
3147
+ truncated: teams.length > listedTeams.length
3112
3148
  }
3113
3149
  };
3114
3150
  } catch (_error) {
@@ -3119,8 +3155,85 @@ ${teamsList.join(`
3119
3155
  }
3120
3156
  };
3121
3157
 
3158
+ // src/search-category.ts
3159
+ var LINEAR_ISSUES_SEARCH_CATEGORY = {
3160
+ category: "linear_issues",
3161
+ label: "Linear issues",
3162
+ description: "Search Linear issues by text and issue metadata filters.",
3163
+ contexts: ["automation", "system"],
3164
+ filters: [
3165
+ { name: "query", label: "Query", type: "string", required: true },
3166
+ {
3167
+ name: "state",
3168
+ label: "States",
3169
+ description: "Issue workflow state names.",
3170
+ type: "string[]"
3171
+ },
3172
+ {
3173
+ name: "assignee",
3174
+ label: "Assignees",
3175
+ description: "Assignee names or emails.",
3176
+ type: "string[]"
3177
+ },
3178
+ {
3179
+ name: "label",
3180
+ label: "Labels",
3181
+ description: "Linear issue label names.",
3182
+ type: "string[]"
3183
+ },
3184
+ {
3185
+ name: "project",
3186
+ label: "Project",
3187
+ description: "Linear project name or identifier.",
3188
+ type: "string"
3189
+ },
3190
+ {
3191
+ name: "team",
3192
+ label: "Team",
3193
+ description: "Linear team key, name, or identifier.",
3194
+ type: "string"
3195
+ },
3196
+ {
3197
+ name: "priority",
3198
+ label: "Priorities",
3199
+ description: "Linear priorities: 1 urgent, 2 high, 3 normal, 4 low.",
3200
+ type: "number[]"
3201
+ },
3202
+ {
3203
+ name: "limit",
3204
+ label: "Limit",
3205
+ description: "Maximum issues to return.",
3206
+ type: "number",
3207
+ default: 10
3208
+ },
3209
+ {
3210
+ name: "accountId",
3211
+ label: "Account",
3212
+ description: "Optional Linear account id. Defaults to LINEAR_DEFAULT_ACCOUNT_ID or the legacy single API key.",
3213
+ type: "string"
3214
+ }
3215
+ ],
3216
+ resultSchemaSummary: "LinearIssue[] with id, identifier, title, description, state, assignee, labels, priority, team, project, url, and updatedAt.",
3217
+ capabilities: ["issues", "filters", "workflow", "team"],
3218
+ source: "plugin:linear",
3219
+ serviceType: "linear"
3220
+ };
3221
+ function hasSearchCategory(runtime, category) {
3222
+ try {
3223
+ runtime.getSearchCategory(category, { includeDisabled: true });
3224
+ return true;
3225
+ } catch {
3226
+ return false;
3227
+ }
3228
+ }
3229
+ function registerLinearSearchCategory(runtime) {
3230
+ if (!hasSearchCategory(runtime, LINEAR_ISSUES_SEARCH_CATEGORY.category)) {
3231
+ runtime.registerSearchCategory(LINEAR_ISSUES_SEARCH_CATEGORY);
3232
+ }
3233
+ }
3234
+
3122
3235
  // src/services/linear.ts
3123
- import { logger as logger11, Service } from "@elizaos/core";
3236
+ import { logger as logger13, Service } from "@elizaos/core";
3124
3237
  import {
3125
3238
  LinearClient
3126
3239
  } from "@linear/sdk";
@@ -3148,44 +3261,64 @@ class LinearAuthenticationError extends LinearAPIError {
3148
3261
  class LinearService extends Service {
3149
3262
  static serviceType = "linear";
3150
3263
  capabilityDescription = "Linear API integration for issue tracking, project management, and team collaboration";
3151
- client;
3264
+ clients = new Map;
3152
3265
  activityLog = [];
3153
- linearConfig;
3266
+ defaultAccountId = DEFAULT_LINEAR_ACCOUNT_ID;
3154
3267
  workspaceId;
3155
3268
  constructor(runtime) {
3156
3269
  super(runtime);
3157
- const apiKey = runtime?.getSetting("LINEAR_API_KEY");
3158
- const workspaceId = runtime?.getSetting("LINEAR_WORKSPACE_ID");
3159
- if (!apiKey) {
3270
+ const accounts = runtime ? readLinearAccounts(runtime) : [];
3271
+ const requestedDefault = runtime ? normalizeLinearAccountId(runtime.getSetting("LINEAR_DEFAULT_ACCOUNT_ID") ?? runtime.getSetting("LINEAR_ACCOUNT_ID")) : DEFAULT_LINEAR_ACCOUNT_ID;
3272
+ const defaultAccount = resolveLinearDefaultAccount(accounts, requestedDefault);
3273
+ if (!defaultAccount) {
3160
3274
  throw new LinearAuthenticationError("Linear API key is required");
3161
3275
  }
3162
- this.linearConfig = {
3163
- LINEAR_API_KEY: apiKey,
3164
- LINEAR_WORKSPACE_ID: workspaceId
3165
- };
3166
- this.workspaceId = workspaceId;
3167
- this.client = new LinearClient({
3168
- apiKey: this.linearConfig.LINEAR_API_KEY
3169
- });
3276
+ this.defaultAccountId = defaultAccount.accountId;
3277
+ this.workspaceId = defaultAccount.workspaceId;
3278
+ for (const account of accounts) {
3279
+ this.clients.set(account.accountId, {
3280
+ accountId: account.accountId,
3281
+ config: account,
3282
+ client: new LinearClient({ apiKey: account.apiKey })
3283
+ });
3284
+ }
3170
3285
  }
3171
3286
  static async start(runtime) {
3172
3287
  const service = new LinearService(runtime);
3173
3288
  await service.validateConnection();
3174
- logger11.info("Linear service started successfully");
3289
+ logger13.info("Linear service started successfully");
3175
3290
  return service;
3176
3291
  }
3177
3292
  async stop() {
3178
3293
  this.activityLog = [];
3179
- logger11.info("Linear service stopped");
3294
+ logger13.info("Linear service stopped");
3180
3295
  }
3181
- async validateConnection() {
3296
+ async validateConnection(accountId) {
3182
3297
  try {
3183
- const viewer = await this.client.viewer;
3184
- logger11.info(`Linear connected as user: ${viewer.email}`);
3298
+ const state = this.getAccountState(accountId);
3299
+ const viewer = await state.client.viewer;
3300
+ logger13.info(`Linear connected as user: ${viewer.email} (accountId=${state.accountId})`);
3185
3301
  } catch (_error) {
3186
3302
  throw new LinearAuthenticationError("Failed to authenticate with Linear API");
3187
3303
  }
3188
3304
  }
3305
+ hasAccount(accountId) {
3306
+ return Boolean(this.getAccountState(accountId, false));
3307
+ }
3308
+ getDefaultTeamKey(accountId) {
3309
+ return this.getAccountState(accountId).config.defaultTeamKey;
3310
+ }
3311
+ getAccountState(accountId, throwOnMissing = true) {
3312
+ const normalized = normalizeLinearAccountId(accountId);
3313
+ const state = accountId ? this.clients.get(normalized) ?? null : this.clients.get(this.defaultAccountId) ?? Array.from(this.clients.values())[0] ?? null;
3314
+ if (!state && throwOnMissing) {
3315
+ throw new LinearAuthenticationError("Linear API key is required");
3316
+ }
3317
+ return state;
3318
+ }
3319
+ getClient(accountId) {
3320
+ return this.getAccountState(accountId)?.client;
3321
+ }
3189
3322
  logActivity(action, resourceType, resourceId, details, success, error) {
3190
3323
  const activity = {
3191
3324
  id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@@ -3202,7 +3335,7 @@ class LinearService extends Service {
3202
3335
  this.activityLog = this.activityLog.slice(-1000);
3203
3336
  }
3204
3337
  }
3205
- getActivityLog(limit, filter) {
3338
+ getActivityLog(limit, filter, accountId) {
3206
3339
  let filtered = [...this.activityLog];
3207
3340
  if (filter) {
3208
3341
  filtered = filtered.filter((item) => {
@@ -3211,25 +3344,36 @@ class LinearService extends Service {
3211
3344
  });
3212
3345
  });
3213
3346
  }
3347
+ if (accountId) {
3348
+ filtered = filtered.filter((item) => item.details.accountId === accountId);
3349
+ }
3214
3350
  return filtered.slice(-(limit || 100));
3215
3351
  }
3216
- clearActivityLog() {
3352
+ clearActivityLog(accountId) {
3353
+ if (accountId) {
3354
+ this.activityLog = this.activityLog.filter((item) => item.details.accountId !== accountId);
3355
+ logger13.info(`Linear activity log cleared for accountId=${accountId}`);
3356
+ return;
3357
+ }
3217
3358
  this.activityLog = [];
3218
- logger11.info("Linear activity log cleared");
3359
+ logger13.info("Linear activity log cleared");
3219
3360
  }
3220
- async getTeams() {
3221
- const teams = await this.client.teams();
3361
+ async getTeams(accountId) {
3362
+ const state = this.getAccountState(accountId);
3363
+ const teams = await state.client.teams();
3222
3364
  const teamList = await teams.nodes;
3223
- this.logActivity("list_teams", "team", "all", { count: teamList.length }, true);
3365
+ this.logActivity("list_teams", "team", "all", { count: teamList.length, accountId: state.accountId }, true);
3224
3366
  return teamList;
3225
3367
  }
3226
- async getTeam(teamId) {
3227
- const team = await this.client.team(teamId);
3228
- this.logActivity("get_team", "team", teamId, { name: team.name }, true);
3368
+ async getTeam(teamId, accountId) {
3369
+ const state = this.getAccountState(accountId);
3370
+ const team = await state.client.team(teamId);
3371
+ this.logActivity("get_team", "team", teamId, { name: team.name, accountId: state.accountId }, true);
3229
3372
  return team;
3230
3373
  }
3231
- async createIssue(input) {
3232
- const issuePayload = await this.client.createIssue({
3374
+ async createIssue(input, accountId) {
3375
+ const state = this.getAccountState(accountId);
3376
+ const issuePayload = await state.client.createIssue({
3233
3377
  title: input.title,
3234
3378
  description: input.description,
3235
3379
  teamId: input.teamId,
@@ -3247,20 +3391,24 @@ class LinearService extends Service {
3247
3391
  }
3248
3392
  this.logActivity("create_issue", "issue", issue.id, {
3249
3393
  title: input.title,
3250
- teamId: input.teamId
3394
+ teamId: input.teamId,
3395
+ accountId: state.accountId
3251
3396
  }, true);
3252
3397
  return issue;
3253
3398
  }
3254
- async getIssue(issueId) {
3255
- const issue = await this.client.issue(issueId);
3399
+ async getIssue(issueId, accountId) {
3400
+ const state = this.getAccountState(accountId);
3401
+ const issue = await state.client.issue(issueId);
3256
3402
  this.logActivity("get_issue", "issue", issueId, {
3257
3403
  title: issue.title,
3258
- identifier: issue.identifier
3404
+ identifier: issue.identifier,
3405
+ accountId: state.accountId
3259
3406
  }, true);
3260
3407
  return issue;
3261
3408
  }
3262
- async updateIssue(issueId, updates) {
3263
- const updatePayload = await this.client.updateIssue(issueId, {
3409
+ async updateIssue(issueId, updates, accountId) {
3410
+ const state = this.getAccountState(accountId);
3411
+ const updatePayload = await state.client.updateIssue(issueId, {
3264
3412
  title: updates.title,
3265
3413
  description: updates.description,
3266
3414
  priority: updates.priority,
@@ -3275,18 +3423,20 @@ class LinearService extends Service {
3275
3423
  if (!issue) {
3276
3424
  throw new Error("Failed to update issue");
3277
3425
  }
3278
- this.logActivity("update_issue", "issue", issueId, updates, true);
3426
+ this.logActivity("update_issue", "issue", issueId, { ...updates, accountId: state.accountId }, true);
3279
3427
  return issue;
3280
3428
  }
3281
- async deleteIssue(issueId) {
3282
- const archivePayload = await this.client.archiveIssue(issueId);
3429
+ async deleteIssue(issueId, accountId) {
3430
+ const state = this.getAccountState(accountId);
3431
+ const archivePayload = await state.client.archiveIssue(issueId);
3283
3432
  const success = await archivePayload.success;
3284
3433
  if (!success) {
3285
3434
  throw new Error("Failed to archive issue");
3286
3435
  }
3287
- this.logActivity("delete_issue", "issue", issueId, { action: "archived" }, true);
3436
+ this.logActivity("delete_issue", "issue", issueId, { action: "archived", accountId: state.accountId }, true);
3288
3437
  }
3289
- async searchIssues(filters) {
3438
+ async searchIssues(filters, accountId) {
3439
+ const state = this.getAccountState(accountId);
3290
3440
  const filterObject = {};
3291
3441
  if (filters.query) {
3292
3442
  filterObject.or = [
@@ -3295,14 +3445,14 @@ class LinearService extends Service {
3295
3445
  ];
3296
3446
  }
3297
3447
  if (filters.team) {
3298
- const teams = await this.getTeams();
3448
+ const teams = await this.getTeams(state.accountId);
3299
3449
  const team = teams.find((t) => t.key.toLowerCase() === filters.team?.toLowerCase() || t.name.toLowerCase() === filters.team?.toLowerCase());
3300
3450
  if (team) {
3301
3451
  filterObject.team = { id: { eq: team.id } };
3302
3452
  }
3303
3453
  }
3304
3454
  if (filters.assignee && filters.assignee.length > 0) {
3305
- const users = await this.getUsers();
3455
+ const users = await this.getUsers(state.accountId);
3306
3456
  const assigneeIds = filters.assignee.map((assigneeName) => {
3307
3457
  const user = users.find((u) => u.email === assigneeName || u.name.toLowerCase().includes(assigneeName.toLowerCase()));
3308
3458
  return user?.id;
@@ -3326,20 +3476,21 @@ class LinearService extends Service {
3326
3476
  }
3327
3477
  };
3328
3478
  }
3329
- const query = this.client.issues({
3479
+ const query = state.client.issues({
3330
3480
  first: filters.limit || 50,
3331
3481
  filter: Object.keys(filterObject).length > 0 ? filterObject : undefined
3332
3482
  });
3333
3483
  const issues = await query;
3334
3484
  const issueList = await issues.nodes;
3335
3485
  this.logActivity("search_issues", "issue", "search", {
3336
- filters: { ...filters },
3486
+ filters: { ...filters, accountId: state.accountId },
3337
3487
  count: issueList.length
3338
3488
  }, true);
3339
3489
  return issueList;
3340
3490
  }
3341
- async createComment(input) {
3342
- const commentPayload = await this.client.createComment({
3491
+ async createComment(input, accountId) {
3492
+ const state = this.getAccountState(accountId);
3493
+ const commentPayload = await state.client.createComment({
3343
3494
  body: input.body,
3344
3495
  issueId: input.issueId
3345
3496
  });
@@ -3349,12 +3500,39 @@ class LinearService extends Service {
3349
3500
  }
3350
3501
  this.logActivity("create_comment", "comment", comment.id, {
3351
3502
  issueId: input.issueId,
3352
- bodyLength: input.body.length
3503
+ bodyLength: input.body.length,
3504
+ accountId: state.accountId
3353
3505
  }, true);
3354
3506
  return comment;
3355
3507
  }
3356
- async getProjects(teamId) {
3357
- const query = this.client.projects({
3508
+ async updateComment(commentId, body, accountId) {
3509
+ const state = this.getAccountState(accountId);
3510
+ const commentPayload = await state.client.updateComment(commentId, {
3511
+ body
3512
+ });
3513
+ const comment = await commentPayload.comment;
3514
+ if (!comment) {
3515
+ throw new Error("Failed to update comment");
3516
+ }
3517
+ this.logActivity("update_comment", "comment", commentId, { bodyLength: body.length, accountId: state.accountId }, true);
3518
+ return comment;
3519
+ }
3520
+ async deleteComment(commentId, accountId) {
3521
+ const state = this.getAccountState(accountId);
3522
+ const payload = await state.client.deleteComment(commentId);
3523
+ if (!payload.success) {
3524
+ throw new Error("Failed to delete comment");
3525
+ }
3526
+ this.logActivity("delete_comment", "comment", commentId, { accountId: state.accountId }, true);
3527
+ }
3528
+ async listComments(issueId, limit = 25, accountId) {
3529
+ const issue = await this.getClient(accountId).issue(issueId);
3530
+ const connection = await issue.comments({ first: Math.min(limit, 100) });
3531
+ return connection.nodes;
3532
+ }
3533
+ async getProjects(teamId, accountId) {
3534
+ const state = this.getAccountState(accountId);
3535
+ const query = state.client.projects({
3358
3536
  first: 100
3359
3537
  });
3360
3538
  const projects = await query;
@@ -3370,44 +3548,54 @@ class LinearService extends Service {
3370
3548
  }
3371
3549
  this.logActivity("list_projects", "project", "all", {
3372
3550
  count: projectList.length,
3373
- teamId
3551
+ teamId,
3552
+ accountId: state.accountId
3374
3553
  }, true);
3375
3554
  return projectList;
3376
3555
  }
3377
- async getProject(projectId) {
3378
- const project = await this.client.project(projectId);
3556
+ async getProject(projectId, accountId) {
3557
+ const state = this.getAccountState(accountId);
3558
+ const project = await state.client.project(projectId);
3379
3559
  this.logActivity("get_project", "project", projectId, {
3380
- name: project.name
3560
+ name: project.name,
3561
+ accountId: state.accountId
3381
3562
  }, true);
3382
3563
  return project;
3383
3564
  }
3384
- async getUsers() {
3385
- const users = await this.client.users();
3565
+ async getUsers(accountId) {
3566
+ const state = this.getAccountState(accountId);
3567
+ const users = await state.client.users();
3386
3568
  const userList = await users.nodes;
3387
3569
  this.logActivity("list_users", "user", "all", {
3388
- count: userList.length
3570
+ count: userList.length,
3571
+ accountId: state.accountId
3389
3572
  }, true);
3390
3573
  return userList;
3391
3574
  }
3392
- async getCurrentUser() {
3393
- const user = await this.client.viewer;
3575
+ async getCurrentUser(accountId) {
3576
+ const state = this.getAccountState(accountId);
3577
+ const user = await state.client.viewer;
3394
3578
  this.logActivity("get_current_user", "user", user.id, {
3395
3579
  email: user.email,
3396
- name: user.name
3580
+ name: user.name,
3581
+ accountId: state.accountId
3397
3582
  }, true);
3398
3583
  return user;
3399
3584
  }
3400
- async getUserTeams() {
3401
- const viewer = await this.client.viewer;
3585
+ async getUserTeams(accountId) {
3586
+ const state = this.getAccountState(accountId);
3587
+ const viewer = await state.client.viewer;
3402
3588
  const teams = await viewer.teams();
3403
3589
  const teamList = await teams.nodes;
3404
3590
  this.logActivity("list_user_teams", "team", viewer.id, {
3405
- count: teamList.length
3591
+ count: teamList.length,
3592
+ accountId: state.accountId
3406
3593
  }, true);
3407
3594
  return teamList;
3408
3595
  }
3409
- async getLabels(teamId) {
3410
- const query = this.client.issueLabels({
3596
+ async getLabels(teamId, accountId) {
3597
+ const state = this.getAccountState(accountId);
3598
+ const query = state.client.issueLabels({
3411
3599
  first: 100,
3412
3600
  filter: teamId ? {
3413
3601
  team: { id: { eq: teamId } }
@@ -3417,19 +3605,22 @@ class LinearService extends Service {
3417
3605
  const labelList = await labels.nodes;
3418
3606
  this.logActivity("list_labels", "label", "all", {
3419
3607
  count: labelList.length,
3420
- teamId
3608
+ teamId,
3609
+ accountId: state.accountId
3421
3610
  }, true);
3422
3611
  return labelList;
3423
3612
  }
3424
- async getWorkflowStates(teamId) {
3425
- const states = await this.client.workflowStates({
3613
+ async getWorkflowStates(teamId, accountId) {
3614
+ const state = this.getAccountState(accountId);
3615
+ const states = await state.client.workflowStates({
3426
3616
  filter: {
3427
3617
  team: { id: { eq: teamId } }
3428
3618
  }
3429
3619
  });
3430
3620
  const stateList = await states.nodes;
3431
3621
  this.logActivity("list_workflow_states", "team", teamId, {
3432
- count: stateList.length
3622
+ count: stateList.length,
3623
+ accountId: state.accountId
3433
3624
  }, true);
3434
3625
  return stateList;
3435
3626
  }
@@ -3440,28 +3631,38 @@ var linearPlugin = {
3440
3631
  name: "@elizaos/plugin-linear-ts",
3441
3632
  description: "Plugin for integrating with Linear issue tracking system",
3442
3633
  services: [LinearService],
3443
- actions: [
3444
- createIssueAction,
3445
- getIssueAction,
3446
- updateIssueAction,
3447
- deleteIssueAction,
3448
- searchIssuesAction,
3449
- createCommentAction,
3450
- listTeamsAction,
3451
- listProjectsAction,
3452
- getActivityAction,
3453
- clearActivityAction
3454
- ],
3634
+ actions: [...promoteSubactionsToActions(linearAction)],
3455
3635
  providers: [
3456
3636
  linearIssuesProvider,
3457
3637
  linearTeamsProvider,
3458
3638
  linearProjectsProvider,
3459
3639
  linearActivityProvider
3460
- ]
3640
+ ],
3641
+ init: async (_config, runtime) => {
3642
+ registerLinearSearchCategory(runtime);
3643
+ try {
3644
+ const manager = getConnectorAccountManager(runtime);
3645
+ manager.registerProvider(createLinearConnectorAccountProvider(runtime));
3646
+ } catch (err) {
3647
+ logger14.warn({
3648
+ src: "plugin:linear",
3649
+ err: err instanceof Error ? err.message : String(err)
3650
+ }, "Failed to register Linear provider with ConnectorAccountManager");
3651
+ }
3652
+ }
3461
3653
  };
3462
3654
  export {
3655
+ resolveLinearDefaultAccount,
3656
+ resolveLinearAccountId,
3657
+ resolveLinearAccount,
3658
+ readLinearAccounts,
3659
+ normalizeLinearAccountId,
3463
3660
  linearPlugin,
3464
- LinearService
3661
+ hasLinearAccountConfig,
3662
+ createLinearConnectorAccountProvider,
3663
+ LinearService,
3664
+ DEFAULT_LINEAR_ACCOUNT_ROLE,
3665
+ DEFAULT_LINEAR_ACCOUNT_ID
3465
3666
  };
3466
3667
 
3467
- //# debugId=1F6135D155E56E4E64756E2164756E21
3668
+ //# debugId=F5689DC5E2BC1C6664756E2164756E21