@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.
- package/dist/index.js +1820 -1619
- package/dist/index.js.map +29 -21
- 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
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
284
|
+
Respond with JSON only. Use this shape:
|
|
158
285
|
{
|
|
159
|
-
"issueId": "Direct issue ID if explicitly mentioned
|
|
160
|
-
"issueDescription": "Description
|
|
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
|
|
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
|
-
|
|
298
|
+
Respond with JSON only. Use this shape:
|
|
171
299
|
{
|
|
172
300
|
"title": "Brief, clear issue title",
|
|
173
|
-
"description": "Detailed description of the issue
|
|
174
|
-
"teamKey": "Team key if mentioned
|
|
175
|
-
"priority":
|
|
176
|
-
"labels": ["
|
|
177
|
-
"assignee": "Assignee username or email if 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
|
-
|
|
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
|
-
|
|
313
|
+
Respond with JSON only:
|
|
186
314
|
{
|
|
187
|
-
"issueId": "The issue identifier
|
|
315
|
+
"issueId": "The issue identifier, such as ENG-123 or COM2-7"
|
|
188
316
|
}
|
|
189
317
|
|
|
190
|
-
|
|
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
|
-
|
|
331
|
+
Respond with JSON only. Use this shape:
|
|
204
332
|
{
|
|
205
333
|
"timeRange": {
|
|
206
|
-
"period": "today
|
|
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
|
|
211
|
-
"resourceTypes": ["issue
|
|
212
|
-
"resourceId": "Specific resource ID if mentioned
|
|
213
|
-
"user": "User name or
|
|
214
|
-
"successFilter": "success
|
|
215
|
-
"limit":
|
|
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
|
-
|
|
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
|
|
236
|
-
"priority": "
|
|
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
|
|
239
|
-
"recency": "latest
|
|
240
|
-
"type": "bug
|
|
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
|
-
|
|
391
|
+
Respond with JSON only. Use this shape:
|
|
312
392
|
{
|
|
313
|
-
"query": "General search text for title
|
|
314
|
-
"states": ["
|
|
315
|
-
"assignees": ["
|
|
316
|
-
"priorities": ["
|
|
317
|
-
"teams": ["
|
|
318
|
-
"labels": ["
|
|
319
|
-
"hasAssignee": true
|
|
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
|
|
322
|
-
"period": "today
|
|
323
|
-
"from": "ISO date if specific
|
|
324
|
-
"to": "ISO date if specific
|
|
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
|
|
328
|
-
"order": "asc
|
|
407
|
+
"field": "created|updated|priority",
|
|
408
|
+
"order": "asc|desc"
|
|
329
409
|
},
|
|
330
|
-
"limit":
|
|
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
|
-
|
|
418
|
+
Respond with JSON only. Use this shape:
|
|
339
419
|
{
|
|
340
|
-
"issueId": "
|
|
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":
|
|
345
|
-
"teamKey": "New team key if moving to another team
|
|
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": "
|
|
348
|
-
"labels": ["
|
|
427
|
+
"status": "todo|in-progress|done|canceled",
|
|
428
|
+
"labels": ["label"]
|
|
349
429
|
}
|
|
350
430
|
}
|
|
351
431
|
|
|
352
|
-
Only include fields that are being updated.
|
|
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
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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 =
|
|
518
|
-
if (parsed.
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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:
|
|
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 "${
|
|
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 =
|
|
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 "${
|
|
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:
|
|
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
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
|
786
|
-
|
|
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
|
|
789
|
-
description: parsed.description
|
|
790
|
-
priority:
|
|
923
|
+
title: getStringValue(parsed.title),
|
|
924
|
+
description: getStringValue(parsed.description),
|
|
925
|
+
priority: getPriorityNumberValue(parsed.priority)
|
|
791
926
|
};
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
const
|
|
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
|
-
|
|
808
|
-
|
|
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
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
|
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
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
|
1070
|
-
|
|
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
|
|
1077
|
-
|
|
1078
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
|
1272
|
-
|
|
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 =
|
|
1281
|
-
if (parsed.
|
|
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
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
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
|
-
|
|
1313
|
-
|
|
1480
|
+
const actionTypes = getStringArrayValue(parsed.actionTypes);
|
|
1481
|
+
if (actionTypes && actionTypes.length > 0) {
|
|
1482
|
+
filters.action = actionTypes[0];
|
|
1314
1483
|
}
|
|
1315
|
-
|
|
1316
|
-
|
|
1484
|
+
const resourceTypes = getStringArrayValue(parsed.resourceTypes);
|
|
1485
|
+
if (resourceTypes && resourceTypes.length > 0) {
|
|
1486
|
+
filters.resource_type = resourceTypes[0];
|
|
1317
1487
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1488
|
+
const resourceId = getStringValue(parsed.resourceId);
|
|
1489
|
+
if (resourceId) {
|
|
1490
|
+
filters.resource_id = resourceId;
|
|
1320
1491
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
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}: ${
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
|
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 =
|
|
1570
|
-
if (parsed.
|
|
1571
|
-
|
|
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
|
-
|
|
1714
|
+
const searchBy = getRecordValue(parsed.searchBy);
|
|
1715
|
+
if (searchBy && Object.keys(searchBy).length > 0) {
|
|
1575
1716
|
const filters = {};
|
|
1576
|
-
|
|
1577
|
-
|
|
1717
|
+
const title = getStringValue(searchBy.title);
|
|
1718
|
+
if (title) {
|
|
1719
|
+
filters.query = title;
|
|
1578
1720
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1721
|
+
const assignee = getStringValue(searchBy.assignee);
|
|
1722
|
+
if (assignee) {
|
|
1723
|
+
filters.assignee = [assignee];
|
|
1581
1724
|
}
|
|
1582
|
-
|
|
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[
|
|
1737
|
+
const priority = priorityMap[priorityValue.toLowerCase()];
|
|
1594
1738
|
if (priority) {
|
|
1595
1739
|
filters.priority = [priority];
|
|
1596
1740
|
}
|
|
1597
1741
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1742
|
+
const team = getStringValue(searchBy.team);
|
|
1743
|
+
if (team) {
|
|
1744
|
+
filters.team = team;
|
|
1600
1745
|
}
|
|
1601
|
-
|
|
1602
|
-
|
|
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:
|
|
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 (
|
|
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 (
|
|
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
|
|
1634
|
-
return `${index + 1}. ${issue.identifier}: ${issue.title} (${
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
1913
|
+
// src/actions/listComments.ts
|
|
1768
1914
|
import {
|
|
1769
|
-
logger as
|
|
1770
|
-
ModelType as ModelType6
|
|
1915
|
+
logger as logger8
|
|
1771
1916
|
} from "@elizaos/core";
|
|
1772
|
-
var
|
|
1773
|
-
name: "
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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: "
|
|
1823
|
-
actions: ["
|
|
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
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
if (
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
-
|
|
1950
|
-
|
|
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
|
-
|
|
2045
|
-
const errorMessage =
|
|
2046
|
-
await callback?.({
|
|
2047
|
-
|
|
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
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
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
|
|
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
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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(
|
|
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
|
|
2480
|
-
|
|
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
|
-
|
|
2486
|
-
|
|
2146
|
+
const states = getStringArrayValue(parsed.states);
|
|
2147
|
+
if (states && states.length > 0) {
|
|
2148
|
+
filters.state = states;
|
|
2487
2149
|
}
|
|
2488
|
-
|
|
2150
|
+
const assignees = getStringArrayValue(parsed.assignees);
|
|
2151
|
+
if (assignees && assignees.length > 0) {
|
|
2489
2152
|
const processedAssignees = [];
|
|
2490
|
-
for (const assignee of
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2526
|
-
|
|
2189
|
+
const teams = getStringArrayValue(parsed.teams);
|
|
2190
|
+
if (teams && teams.length > 0) {
|
|
2191
|
+
filters.team = teams[0];
|
|
2527
2192
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
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/
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
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
|
-
"
|
|
2638
|
-
"
|
|
2639
|
-
"
|
|
2640
|
-
"
|
|
2641
|
-
"
|
|
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: "
|
|
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
|
|
2655
|
-
actions: ["
|
|
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: "
|
|
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: "
|
|
2677
|
-
content: {
|
|
2678
|
-
|
|
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: "
|
|
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
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
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
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
const
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
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
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
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
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
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
|
-
}
|
|
2931
|
-
|
|
2932
|
-
const
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
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
|
-
|
|
2939
|
-
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
-
|
|
3264
|
+
clients = new Map;
|
|
3152
3265
|
activityLog = [];
|
|
3153
|
-
|
|
3266
|
+
defaultAccountId = DEFAULT_LINEAR_ACCOUNT_ID;
|
|
3154
3267
|
workspaceId;
|
|
3155
3268
|
constructor(runtime) {
|
|
3156
3269
|
super(runtime);
|
|
3157
|
-
const
|
|
3158
|
-
const
|
|
3159
|
-
|
|
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.
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
-
|
|
3289
|
+
logger13.info("Linear service started successfully");
|
|
3175
3290
|
return service;
|
|
3176
3291
|
}
|
|
3177
3292
|
async stop() {
|
|
3178
3293
|
this.activityLog = [];
|
|
3179
|
-
|
|
3294
|
+
logger13.info("Linear service stopped");
|
|
3180
3295
|
}
|
|
3181
|
-
async validateConnection() {
|
|
3296
|
+
async validateConnection(accountId) {
|
|
3182
3297
|
try {
|
|
3183
|
-
const
|
|
3184
|
-
|
|
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
|
-
|
|
3359
|
+
logger13.info("Linear activity log cleared");
|
|
3219
3360
|
}
|
|
3220
|
-
async getTeams() {
|
|
3221
|
-
const
|
|
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
|
|
3228
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
3357
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
3661
|
+
hasLinearAccountConfig,
|
|
3662
|
+
createLinearConnectorAccountProvider,
|
|
3663
|
+
LinearService,
|
|
3664
|
+
DEFAULT_LINEAR_ACCOUNT_ROLE,
|
|
3665
|
+
DEFAULT_LINEAR_ACCOUNT_ID
|
|
3465
3666
|
};
|
|
3466
3667
|
|
|
3467
|
-
//# debugId=
|
|
3668
|
+
//# debugId=F5689DC5E2BC1C6664756E2164756E21
|