@elizaos/plugin-linear 2.0.0-alpha.6 → 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 +1808 -1187
- 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,38 +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((kw) => kw.length > 0 && __avText.includes(kw));
|
|
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);
|
|
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, _message, _state) => {
|
|
57
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
58
|
-
return !!apiKey;
|
|
59
|
-
};
|
|
60
|
-
try {
|
|
61
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
62
|
-
} catch {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
},
|
|
203
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
204
|
+
keywords: ["clear", "linear", "activity"],
|
|
205
|
+
regexAlternation: "clear|linear|activity"
|
|
206
|
+
}),
|
|
66
207
|
async handler(runtime, message, _state, _options, callback) {
|
|
67
208
|
try {
|
|
68
209
|
const linearService = runtime.getService("linear");
|
|
69
210
|
if (!linearService) {
|
|
70
211
|
throw new Error("Linear service not available");
|
|
71
212
|
}
|
|
72
|
-
|
|
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
|
+
]);
|
|
73
242
|
const successMessage = "✅ Linear activity log has been cleared.";
|
|
74
243
|
await callback?.({
|
|
75
244
|
text: successMessage,
|
|
@@ -100,7 +269,7 @@ import {
|
|
|
100
269
|
ModelType
|
|
101
270
|
} from "@elizaos/core";
|
|
102
271
|
|
|
103
|
-
// src/
|
|
272
|
+
// src/prompts.ts
|
|
104
273
|
var createCommentTemplate = `Extract comment details from the user's request to add a comment to a Linear issue.
|
|
105
274
|
|
|
106
275
|
User request: "{{userMessage}}"
|
|
@@ -112,40 +281,41 @@ The user might express this in various ways:
|
|
|
112
281
|
- "Reply to COM2-7: Thanks for the update"
|
|
113
282
|
- "Let the payment issue know that it's blocked by API changes"
|
|
114
283
|
|
|
115
|
-
|
|
284
|
+
Respond with JSON only. Use this shape:
|
|
116
285
|
{
|
|
117
|
-
"issueId": "Direct issue ID if explicitly mentioned
|
|
118
|
-
"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",
|
|
119
288
|
"commentBody": "The actual comment content to add",
|
|
120
|
-
"commentType": "note
|
|
289
|
+
"commentType": "note|reply|update|question|feedback"
|
|
121
290
|
}
|
|
122
291
|
|
|
123
|
-
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.`;
|
|
124
294
|
var createIssueTemplate = `Given the user's request, extract the information needed to create a Linear issue.
|
|
125
295
|
|
|
126
296
|
User request: "{{userMessage}}"
|
|
127
297
|
|
|
128
|
-
|
|
298
|
+
Respond with JSON only. Use this shape:
|
|
129
299
|
{
|
|
130
300
|
"title": "Brief, clear issue title",
|
|
131
|
-
"description": "Detailed description of the issue
|
|
132
|
-
"teamKey": "Team key if mentioned
|
|
133
|
-
"priority":
|
|
134
|
-
"labels": ["
|
|
135
|
-
"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"
|
|
136
306
|
}
|
|
137
307
|
|
|
138
|
-
|
|
308
|
+
Omit optional fields when they are not provided. Output only the JSON object, with no prose before or after it.`;
|
|
139
309
|
var deleteIssueTemplate = `Given the user's request to delete/archive a Linear issue, extract the issue identifier.
|
|
140
310
|
|
|
141
311
|
User request: "{{userMessage}}"
|
|
142
312
|
|
|
143
|
-
|
|
313
|
+
Respond with JSON only:
|
|
144
314
|
{
|
|
145
|
-
"issueId": "The issue identifier
|
|
315
|
+
"issueId": "The issue identifier, such as ENG-123 or COM2-7"
|
|
146
316
|
}
|
|
147
317
|
|
|
148
|
-
|
|
318
|
+
Output only the JSON object, with no prose before or after it.`;
|
|
149
319
|
var getActivityTemplate = `Extract activity filter criteria from the user's request.
|
|
150
320
|
|
|
151
321
|
User request: "{{userMessage}}"
|
|
@@ -158,22 +328,22 @@ The user might ask for activity in various ways:
|
|
|
158
328
|
- "Recent comment activity" → action type + recency
|
|
159
329
|
- "Failed operations this week" → success filter + time range
|
|
160
330
|
|
|
161
|
-
|
|
331
|
+
Respond with JSON only. Use this shape:
|
|
162
332
|
{
|
|
163
333
|
"timeRange": {
|
|
164
|
-
"period": "today
|
|
165
|
-
"from": "ISO datetime if specific",
|
|
166
|
-
"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"
|
|
167
337
|
},
|
|
168
|
-
"actionTypes": ["create_issue
|
|
169
|
-
"resourceTypes": ["issue
|
|
170
|
-
"resourceId": "Specific resource ID if mentioned
|
|
171
|
-
"user": "User name or
|
|
172
|
-
"successFilter": "success
|
|
173
|
-
"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
|
|
174
344
|
}
|
|
175
345
|
|
|
176
|
-
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.`;
|
|
177
347
|
var getIssueTemplate = `Extract issue identification from the user's request.
|
|
178
348
|
|
|
179
349
|
User request: "{{userMessage}}"
|
|
@@ -185,73 +355,25 @@ The user might reference an issue by:
|
|
|
185
355
|
- Recency (e.g., "the latest bug", "most recent issue")
|
|
186
356
|
- Team context (e.g., "newest issue in ELIZA team")
|
|
187
357
|
|
|
188
|
-
|
|
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:
|
|
189
364
|
{
|
|
190
|
-
"directId": "Issue ID if explicitly mentioned (e.g., ENG-123)",
|
|
191
365
|
"searchBy": {
|
|
192
366
|
"title": "Keywords from issue title if mentioned",
|
|
193
|
-
"assignee": "Name
|
|
194
|
-
"priority": "
|
|
367
|
+
"assignee": "Name or email of assignee if mentioned",
|
|
368
|
+
"priority": "urgent|high|normal|low|1|2|3|4",
|
|
195
369
|
"team": "Team name or key if mentioned",
|
|
196
|
-
"state": "Issue state if mentioned
|
|
197
|
-
"recency": "latest
|
|
198
|
-
"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"
|
|
199
373
|
}
|
|
200
374
|
}
|
|
201
375
|
|
|
202
|
-
Only include fields that are clearly mentioned or implied.`;
|
|
203
|
-
var listProjectsTemplate = `Extract project filter criteria from the user's request.
|
|
204
|
-
|
|
205
|
-
User request: "{{userMessage}}"
|
|
206
|
-
|
|
207
|
-
The user might ask for projects in various ways:
|
|
208
|
-
- "Show me all projects" → list all projects
|
|
209
|
-
- "Active projects" → filter by state (active/planned/completed)
|
|
210
|
-
- "Projects due this quarter" → filter by target date
|
|
211
|
-
- "Which projects is Sarah managing?" → filter by lead/owner
|
|
212
|
-
- "Projects with high priority issues" → filter by contained issue priority
|
|
213
|
-
- "Projects for the engineering team" → filter by team
|
|
214
|
-
- "Completed projects" → filter by state
|
|
215
|
-
- "Projects starting next month" → filter by start date
|
|
216
|
-
|
|
217
|
-
Return ONLY a JSON object:
|
|
218
|
-
{
|
|
219
|
-
"teamFilter": "Team name or key if mentioned",
|
|
220
|
-
"stateFilter": "active/planned/completed/all",
|
|
221
|
-
"dateFilter": {
|
|
222
|
-
"type": "due/starting",
|
|
223
|
-
"period": "this-week/this-month/this-quarter/next-month/next-quarter",
|
|
224
|
-
"from": "ISO date if specific",
|
|
225
|
-
"to": "ISO date if specific"
|
|
226
|
-
},
|
|
227
|
-
"leadFilter": "Project lead name if mentioned",
|
|
228
|
-
"showAll": true/false (true if user explicitly asks for "all")
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
Only include fields that are clearly mentioned.`;
|
|
232
|
-
var listTeamsTemplate = `Extract team filter criteria from the user's request.
|
|
233
|
-
|
|
234
|
-
User request: "{{userMessage}}"
|
|
235
|
-
|
|
236
|
-
The user might ask for teams in various ways:
|
|
237
|
-
- "Show me all teams" → list all teams
|
|
238
|
-
- "Engineering teams" → filter by teams with engineering in name/description
|
|
239
|
-
- "List teams I'm part of" → filter by membership
|
|
240
|
-
- "Which teams work on the mobile app?" → filter by description/focus
|
|
241
|
-
- "Show me the ELIZA team details" → specific team lookup
|
|
242
|
-
- "Active teams" → teams with recent activity
|
|
243
|
-
- "Frontend and backend teams" → multiple team types
|
|
244
|
-
|
|
245
|
-
Return ONLY a JSON object:
|
|
246
|
-
{
|
|
247
|
-
"nameFilter": "Keywords to search in team names",
|
|
248
|
-
"specificTeam": "Specific team name or key if looking for one team",
|
|
249
|
-
"myTeams": true/false (true if user wants their teams),
|
|
250
|
-
"showAll": true/false (true if user explicitly asks for "all"),
|
|
251
|
-
"includeDetails": true/false (true if user wants detailed info)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
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.`;
|
|
255
377
|
var searchIssuesTemplate = `Extract search criteria from the user's request for Linear issues.
|
|
256
378
|
|
|
257
379
|
User request: "{{userMessage}}"
|
|
@@ -266,53 +388,199 @@ The user might express searches in various ways:
|
|
|
266
388
|
- "Bugs that are almost done" → label + state filter
|
|
267
389
|
- "Show me the oldest open issues" → state + sort order
|
|
268
390
|
|
|
269
|
-
|
|
391
|
+
Respond with JSON only. Use this shape:
|
|
270
392
|
{
|
|
271
|
-
"query": "General search text for title
|
|
272
|
-
"states": ["
|
|
273
|
-
"assignees": ["
|
|
274
|
-
"priorities": ["
|
|
275
|
-
"teams": ["
|
|
276
|
-
"labels": ["
|
|
277
|
-
"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,
|
|
278
400
|
"dateRange": {
|
|
279
|
-
"field": "created
|
|
280
|
-
"period": "today
|
|
281
|
-
"from": "ISO date if specific
|
|
282
|
-
"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"
|
|
283
405
|
},
|
|
284
406
|
"sort": {
|
|
285
|
-
"field": "created
|
|
286
|
-
"order": "asc
|
|
407
|
+
"field": "created|updated|priority",
|
|
408
|
+
"order": "asc|desc"
|
|
287
409
|
},
|
|
288
|
-
"limit":
|
|
410
|
+
"limit": 10
|
|
289
411
|
}
|
|
290
412
|
|
|
291
|
-
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.`;
|
|
292
414
|
var updateIssueTemplate = `Given the user's request to update a Linear issue, extract the information needed.
|
|
293
415
|
|
|
294
416
|
User request: "{{userMessage}}"
|
|
295
417
|
|
|
296
|
-
|
|
418
|
+
Respond with JSON only. Use this shape:
|
|
297
419
|
{
|
|
298
|
-
"issueId": "
|
|
420
|
+
"issueId": "Issue identifier such as ENG-123 or COM2-7",
|
|
299
421
|
"updates": {
|
|
300
422
|
"title": "New title if changing the title",
|
|
301
423
|
"description": "New description if changing the description",
|
|
302
|
-
"priority":
|
|
303
|
-
"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",
|
|
304
426
|
"assignee": "New assignee username or email if changing",
|
|
305
|
-
"status": "
|
|
306
|
-
"labels": ["
|
|
427
|
+
"status": "todo|in-progress|done|canceled",
|
|
428
|
+
"labels": ["label"]
|
|
307
429
|
}
|
|
308
430
|
}
|
|
309
431
|
|
|
310
|
-
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
|
+
}
|
|
311
560
|
|
|
312
561
|
// src/actions/createComment.ts
|
|
313
562
|
var createCommentAction = {
|
|
314
563
|
name: "CREATE_LINEAR_COMMENT",
|
|
564
|
+
contexts: ["tasks", "connectors", "automation"],
|
|
565
|
+
contextGate: { anyOf: ["tasks", "connectors", "automation"] },
|
|
566
|
+
roleGate: { minRole: "USER" },
|
|
315
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
|
+
],
|
|
316
584
|
similes: [
|
|
317
585
|
"create-linear-comment",
|
|
318
586
|
"add-linear-comment",
|
|
@@ -366,37 +634,17 @@ var createCommentAction = {
|
|
|
366
634
|
}
|
|
367
635
|
]
|
|
368
636
|
],
|
|
369
|
-
validate: async (runtime, message, state
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
374
|
-
const __avRegex = /\b(?:create|linear|comment)\b/i;
|
|
375
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
376
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
377
|
-
const __avExpectedSource = "";
|
|
378
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
379
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
380
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
381
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
382
|
-
return false;
|
|
383
|
-
}
|
|
384
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
385
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
386
|
-
return !!apiKey;
|
|
387
|
-
};
|
|
388
|
-
try {
|
|
389
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
390
|
-
} catch {
|
|
391
|
-
return false;
|
|
392
|
-
}
|
|
393
|
-
},
|
|
637
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
638
|
+
keywords: ["create", "linear", "comment"],
|
|
639
|
+
regexAlternation: "create|linear|comment"
|
|
640
|
+
}),
|
|
394
641
|
async handler(runtime, message, _state, _options, callback) {
|
|
395
642
|
try {
|
|
396
643
|
const linearService = runtime.getService("linear");
|
|
397
644
|
if (!linearService) {
|
|
398
645
|
throw new Error("Linear service not available");
|
|
399
646
|
}
|
|
647
|
+
const accountId = getLinearAccountId(runtime, _options);
|
|
400
648
|
const content = message.content.text;
|
|
401
649
|
if (!content) {
|
|
402
650
|
const errorMessage = "Please provide a message with the issue and comment content.";
|
|
@@ -430,22 +678,28 @@ var createCommentAction = {
|
|
|
430
678
|
}
|
|
431
679
|
} else {
|
|
432
680
|
try {
|
|
433
|
-
const parsed =
|
|
434
|
-
if (parsed.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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) {
|
|
438
692
|
const filters = {
|
|
439
|
-
query:
|
|
693
|
+
query: issueDescription,
|
|
440
694
|
limit: 5
|
|
441
695
|
};
|
|
442
|
-
const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
696
|
+
const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
443
697
|
if (defaultTeamKey) {
|
|
444
698
|
filters.team = defaultTeamKey;
|
|
445
699
|
}
|
|
446
|
-
const issues = await linearService.searchIssues(filters);
|
|
700
|
+
const issues = await linearService.searchIssues(filters, accountId);
|
|
447
701
|
if (issues.length === 0) {
|
|
448
|
-
const errorMessage = `No issues found matching "${
|
|
702
|
+
const errorMessage = `No issues found matching "${issueDescription}". Please provide a specific issue ID.`;
|
|
449
703
|
await callback?.({
|
|
450
704
|
text: errorMessage,
|
|
451
705
|
source: message.content.source
|
|
@@ -457,13 +711,13 @@ var createCommentAction = {
|
|
|
457
711
|
}
|
|
458
712
|
if (issues.length === 1) {
|
|
459
713
|
issueId = issues[0].identifier;
|
|
460
|
-
commentBody =
|
|
714
|
+
commentBody = parsedCommentBody;
|
|
461
715
|
} else {
|
|
462
716
|
const issueList = await Promise.all(issues.map(async (issue2, index) => {
|
|
463
717
|
const state = await issue2.state;
|
|
464
718
|
return `${index + 1}. ${issue2.identifier}: ${issue2.title} (${state?.name || "No state"})`;
|
|
465
719
|
}));
|
|
466
|
-
const clarifyMessage = `Found multiple issues matching "${
|
|
720
|
+
const clarifyMessage = `Found multiple issues matching "${issueDescription}":
|
|
467
721
|
${issueList.join(`
|
|
468
722
|
`)}
|
|
469
723
|
|
|
@@ -482,15 +736,16 @@ Please specify which issue to comment on by its ID.`;
|
|
|
482
736
|
identifier: i.identifier,
|
|
483
737
|
title: i.title
|
|
484
738
|
})),
|
|
485
|
-
pendingComment:
|
|
739
|
+
pendingComment: parsedCommentBody
|
|
486
740
|
}
|
|
487
741
|
};
|
|
488
742
|
}
|
|
489
743
|
} else {
|
|
490
744
|
throw new Error("No issue identifier or description found");
|
|
491
745
|
}
|
|
492
|
-
|
|
493
|
-
|
|
746
|
+
const commentType = getStringValue(parsed.commentType)?.toLowerCase();
|
|
747
|
+
if (commentType && commentType !== "note") {
|
|
748
|
+
commentBody = `[${commentType.toUpperCase()}] ${commentBody}`;
|
|
494
749
|
}
|
|
495
750
|
} catch (parseError) {
|
|
496
751
|
logger2.warn("Failed to parse LLM response, falling back to regex:", parseError);
|
|
@@ -522,11 +777,11 @@ Please specify which issue to comment on by its ID.`;
|
|
|
522
777
|
success: false
|
|
523
778
|
};
|
|
524
779
|
}
|
|
525
|
-
const issue = await linearService.getIssue(issueId);
|
|
780
|
+
const issue = await linearService.getIssue(issueId, accountId);
|
|
526
781
|
const comment = await linearService.createComment({
|
|
527
782
|
issueId: issue.id,
|
|
528
783
|
body: commentBody
|
|
529
|
-
});
|
|
784
|
+
}, accountId);
|
|
530
785
|
const successMessage = `✅ Comment added to issue ${issue.identifier}: "${commentBody}"`;
|
|
531
786
|
await callback?.({
|
|
532
787
|
text: successMessage,
|
|
@@ -540,7 +795,8 @@ Please specify which issue to comment on by its ID.`;
|
|
|
540
795
|
issueId: issue.id,
|
|
541
796
|
issueIdentifier: issue.identifier,
|
|
542
797
|
commentBody,
|
|
543
|
-
createdAt: comment.createdAt instanceof Date ? comment.createdAt.toISOString() : comment.createdAt
|
|
798
|
+
createdAt: comment.createdAt instanceof Date ? comment.createdAt.toISOString() : comment.createdAt,
|
|
799
|
+
accountId
|
|
544
800
|
}
|
|
545
801
|
};
|
|
546
802
|
} catch (error) {
|
|
@@ -565,7 +821,30 @@ import {
|
|
|
565
821
|
} from "@elizaos/core";
|
|
566
822
|
var createIssueAction = {
|
|
567
823
|
name: "CREATE_LINEAR_ISSUE",
|
|
568
|
-
|
|
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
|
+
],
|
|
569
848
|
similes: ["create-linear-issue", "new-linear-issue", "add-linear-issue"],
|
|
570
849
|
examples: [
|
|
571
850
|
[
|
|
@@ -599,37 +878,17 @@ var createIssueAction = {
|
|
|
599
878
|
}
|
|
600
879
|
]
|
|
601
880
|
],
|
|
602
|
-
validate: async (runtime, message, state
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
607
|
-
const __avRegex = /\b(?:create|linear|issue)\b/i;
|
|
608
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
609
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
610
|
-
const __avExpectedSource = "";
|
|
611
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
612
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
613
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
614
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
618
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
619
|
-
return !!apiKey;
|
|
620
|
-
};
|
|
621
|
-
try {
|
|
622
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
623
|
-
} catch {
|
|
624
|
-
return false;
|
|
625
|
-
}
|
|
626
|
-
},
|
|
881
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
882
|
+
keywords: ["create", "linear", "issue"],
|
|
883
|
+
regexAlternation: "create|linear|issue"
|
|
884
|
+
}),
|
|
627
885
|
async handler(runtime, message, _state, _options, callback) {
|
|
628
886
|
try {
|
|
629
887
|
const linearService = runtime.getService("linear");
|
|
630
888
|
if (!linearService) {
|
|
631
889
|
throw new Error("Linear service not available");
|
|
632
890
|
}
|
|
891
|
+
const accountId = getLinearAccountId(runtime, _options);
|
|
633
892
|
const content = message.content.text;
|
|
634
893
|
if (!content) {
|
|
635
894
|
const errorMessage = "Please provide a description for the issue.";
|
|
@@ -656,37 +915,40 @@ var createIssueAction = {
|
|
|
656
915
|
throw new Error("Failed to extract issue information");
|
|
657
916
|
}
|
|
658
917
|
try {
|
|
659
|
-
const
|
|
660
|
-
|
|
918
|
+
const parsed = parseLinearPromptResponse(response);
|
|
919
|
+
if (Object.keys(parsed).length === 0) {
|
|
920
|
+
throw new Error("No fields found in model response");
|
|
921
|
+
}
|
|
661
922
|
issueData = {
|
|
662
|
-
title: parsed.title
|
|
663
|
-
description: parsed.description
|
|
664
|
-
priority:
|
|
923
|
+
title: getStringValue(parsed.title),
|
|
924
|
+
description: getStringValue(parsed.description),
|
|
925
|
+
priority: getPriorityNumberValue(parsed.priority)
|
|
665
926
|
};
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
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());
|
|
669
931
|
if (team) {
|
|
670
932
|
issueData.teamId = team.id;
|
|
671
933
|
}
|
|
672
934
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
935
|
+
const assignee = getStringValue(parsed.assignee);
|
|
936
|
+
if (assignee) {
|
|
937
|
+
const cleanAssignee = assignee.replace(/^@/, "");
|
|
938
|
+
const users = await linearService.getUsers(accountId);
|
|
676
939
|
const user = users.find((u) => u.email === cleanAssignee || u.name.toLowerCase().includes(cleanAssignee.toLowerCase()));
|
|
677
940
|
if (user) {
|
|
678
941
|
issueData.assigneeId = user.id;
|
|
679
942
|
}
|
|
680
943
|
}
|
|
681
|
-
|
|
682
|
-
|
|
944
|
+
const parsedLabels = getStringArrayValue(parsed.labels);
|
|
945
|
+
if (parsedLabels && parsedLabels.length > 0) {
|
|
946
|
+
const labels = await linearService.getLabels(issueData.teamId, accountId);
|
|
683
947
|
const labelIds = [];
|
|
684
|
-
for (const labelName of
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
labelIds.push(label.id);
|
|
689
|
-
}
|
|
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);
|
|
690
952
|
}
|
|
691
953
|
}
|
|
692
954
|
if (labelIds.length > 0) {
|
|
@@ -694,9 +956,9 @@ var createIssueAction = {
|
|
|
694
956
|
}
|
|
695
957
|
}
|
|
696
958
|
if (!issueData.teamId) {
|
|
697
|
-
const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
959
|
+
const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
698
960
|
if (defaultTeamKey) {
|
|
699
|
-
const teams = await linearService.getTeams();
|
|
961
|
+
const teams = await linearService.getTeams(accountId);
|
|
700
962
|
const defaultTeam = teams.find((t) => t.key.toLowerCase() === defaultTeamKey.toLowerCase());
|
|
701
963
|
if (defaultTeam) {
|
|
702
964
|
issueData.teamId = defaultTeam.id;
|
|
@@ -706,7 +968,7 @@ var createIssueAction = {
|
|
|
706
968
|
}
|
|
707
969
|
}
|
|
708
970
|
if (!issueData.teamId) {
|
|
709
|
-
const teams = await linearService.getTeams();
|
|
971
|
+
const teams = await linearService.getTeams(accountId);
|
|
710
972
|
if (teams.length > 0) {
|
|
711
973
|
issueData.teamId = teams[0].id;
|
|
712
974
|
logger3.warn(`No team specified, using first available team: ${teams[0].name}`);
|
|
@@ -719,8 +981,8 @@ var createIssueAction = {
|
|
|
719
981
|
title: content.length > 100 ? `${content.substring(0, 100)}...` : content,
|
|
720
982
|
description: content
|
|
721
983
|
};
|
|
722
|
-
const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
723
|
-
const teams = await linearService.getTeams();
|
|
984
|
+
const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
985
|
+
const teams = await linearService.getTeams(accountId);
|
|
724
986
|
if (defaultTeamKey) {
|
|
725
987
|
const defaultTeam = teams.find((t) => t.key.toLowerCase() === defaultTeamKey.toLowerCase());
|
|
726
988
|
if (defaultTeam) {
|
|
@@ -756,7 +1018,7 @@ var createIssueAction = {
|
|
|
756
1018
|
success: false
|
|
757
1019
|
};
|
|
758
1020
|
}
|
|
759
|
-
const issue = await linearService.createIssue(issueData);
|
|
1021
|
+
const issue = await linearService.createIssue(issueData, accountId);
|
|
760
1022
|
const successMessage = `✅ Created Linear issue: ${issue.title} (${issue.identifier})
|
|
761
1023
|
|
|
762
1024
|
View it at: ${issue.url}`;
|
|
@@ -770,7 +1032,8 @@ View it at: ${issue.url}`;
|
|
|
770
1032
|
data: {
|
|
771
1033
|
issueId: issue.id,
|
|
772
1034
|
identifier: issue.identifier,
|
|
773
|
-
url: issue.url
|
|
1035
|
+
url: issue.url,
|
|
1036
|
+
accountId
|
|
774
1037
|
}
|
|
775
1038
|
};
|
|
776
1039
|
} catch (error) {
|
|
@@ -788,14 +1051,101 @@ View it at: ${issue.url}`;
|
|
|
788
1051
|
}
|
|
789
1052
|
};
|
|
790
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
|
+
|
|
791
1125
|
// src/actions/deleteIssue.ts
|
|
792
1126
|
import {
|
|
793
|
-
logger as
|
|
794
|
-
ModelType as ModelType3
|
|
1127
|
+
logger as logger5,
|
|
1128
|
+
ModelType as ModelType3,
|
|
1129
|
+
requireConfirmation as requireConfirmation2
|
|
795
1130
|
} from "@elizaos/core";
|
|
1131
|
+
var LINEAR_MODEL_TIMEOUT_MS = 15000;
|
|
1132
|
+
var LINEAR_ISSUE_TITLE_MAX_CHARS = 300;
|
|
796
1133
|
var deleteIssueAction = {
|
|
797
1134
|
name: "DELETE_LINEAR_ISSUE",
|
|
1135
|
+
contexts: ["tasks", "connectors", "automation"],
|
|
1136
|
+
contextGate: { anyOf: ["tasks", "connectors", "automation"] },
|
|
1137
|
+
roleGate: { minRole: "USER" },
|
|
798
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
|
+
],
|
|
799
1149
|
similes: [
|
|
800
1150
|
"delete-linear-issue",
|
|
801
1151
|
"archive-linear-issue",
|
|
@@ -849,37 +1199,17 @@ var deleteIssueAction = {
|
|
|
849
1199
|
}
|
|
850
1200
|
]
|
|
851
1201
|
],
|
|
852
|
-
validate: async (runtime, message, state
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
857
|
-
const __avRegex = /\b(?:delete|linear|issue)\b/i;
|
|
858
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
859
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
860
|
-
const __avExpectedSource = "";
|
|
861
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
862
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
863
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
864
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
868
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
869
|
-
return !!apiKey;
|
|
870
|
-
};
|
|
871
|
-
try {
|
|
872
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
873
|
-
} catch {
|
|
874
|
-
return false;
|
|
875
|
-
}
|
|
876
|
-
},
|
|
1202
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
1203
|
+
keywords: ["delete", "linear", "issue"],
|
|
1204
|
+
regexAlternation: "delete|linear|issue"
|
|
1205
|
+
}),
|
|
877
1206
|
async handler(runtime, message, _state, _options, callback) {
|
|
878
1207
|
try {
|
|
879
1208
|
const linearService = runtime.getService("linear");
|
|
880
1209
|
if (!linearService) {
|
|
881
1210
|
throw new Error("Linear service not available");
|
|
882
1211
|
}
|
|
1212
|
+
const accountId = getLinearAccountId(runtime, _options);
|
|
883
1213
|
const content = message.content.text;
|
|
884
1214
|
if (!content) {
|
|
885
1215
|
const errorMessage = "Please specify which issue to delete.";
|
|
@@ -898,21 +1228,26 @@ var deleteIssueAction = {
|
|
|
898
1228
|
issueId = params.issueId;
|
|
899
1229
|
} else {
|
|
900
1230
|
const prompt = deleteIssueTemplate.replace("{{userMessage}}", content);
|
|
901
|
-
const response = await
|
|
902
|
-
|
|
903
|
-
|
|
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
|
+
]);
|
|
904
1237
|
if (!response) {
|
|
905
1238
|
throw new Error("Failed to extract issue identifier");
|
|
906
1239
|
}
|
|
907
1240
|
try {
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
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) ?? "";
|
|
911
1246
|
if (!issueId) {
|
|
912
1247
|
throw new Error("Issue ID not found in parsed response");
|
|
913
1248
|
}
|
|
914
1249
|
} catch (parseError) {
|
|
915
|
-
|
|
1250
|
+
logger5.warn("Failed to parse LLM response, falling back to regex parsing:", parseError);
|
|
916
1251
|
const issueMatch = content.match(/(\w+-\d+)/);
|
|
917
1252
|
if (!issueMatch) {
|
|
918
1253
|
const errorMessage = "Please specify an issue ID (e.g., ENG-123) to delete.";
|
|
@@ -928,11 +1263,35 @@ var deleteIssueAction = {
|
|
|
928
1263
|
issueId = issueMatch[1];
|
|
929
1264
|
}
|
|
930
1265
|
}
|
|
931
|
-
const issue = await linearService.getIssue(issueId);
|
|
932
|
-
const issueTitle = issue.title;
|
|
1266
|
+
const issue = await linearService.getIssue(issueId, accountId);
|
|
1267
|
+
const issueTitle = issue.title.slice(0, LINEAR_ISSUE_TITLE_MAX_CHARS);
|
|
933
1268
|
const issueIdentifier = issue.identifier;
|
|
934
|
-
|
|
935
|
-
|
|
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);
|
|
936
1295
|
const successMessage = `✅ Successfully archived issue ${issueIdentifier}: "${issueTitle}"
|
|
937
1296
|
|
|
938
1297
|
The issue has been moved to the archived state and will no longer appear in active views.`;
|
|
@@ -947,11 +1306,12 @@ The issue has been moved to the archived state and will no longer appear in acti
|
|
|
947
1306
|
issueId: issue.id,
|
|
948
1307
|
identifier: issueIdentifier,
|
|
949
1308
|
title: issueTitle,
|
|
950
|
-
archived: true
|
|
1309
|
+
archived: true,
|
|
1310
|
+
accountId
|
|
951
1311
|
}
|
|
952
1312
|
};
|
|
953
1313
|
} catch (error) {
|
|
954
|
-
|
|
1314
|
+
logger5.error("Failed to delete issue:", error);
|
|
955
1315
|
const errorMessage = `❌ Failed to delete issue: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
956
1316
|
await callback?.({
|
|
957
1317
|
text: errorMessage,
|
|
@@ -967,18 +1327,49 @@ The issue has been moved to the archived state and will no longer appear in acti
|
|
|
967
1327
|
|
|
968
1328
|
// src/actions/getActivity.ts
|
|
969
1329
|
import {
|
|
970
|
-
logger as
|
|
1330
|
+
logger as logger6,
|
|
971
1331
|
ModelType as ModelType4
|
|
972
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
|
+
}
|
|
973
1345
|
var getActivityAction = {
|
|
974
1346
|
name: "GET_LINEAR_ACTIVITY",
|
|
1347
|
+
contexts: ["tasks", "connectors", "automation"],
|
|
1348
|
+
contextGate: { anyOf: ["tasks", "connectors", "automation"] },
|
|
1349
|
+
roleGate: { minRole: "USER" },
|
|
975
1350
|
description: "Get recent Linear activity log with optional filters",
|
|
1351
|
+
descriptionCompressed: "get recent Linear activity log w/ optional filter",
|
|
976
1352
|
similes: [
|
|
977
1353
|
"get-linear-activity",
|
|
978
1354
|
"show-linear-activity",
|
|
979
1355
|
"view-linear-activity",
|
|
980
1356
|
"check-linear-activity"
|
|
981
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
|
+
],
|
|
982
1373
|
examples: [
|
|
983
1374
|
[
|
|
984
1375
|
{
|
|
@@ -1026,40 +1417,21 @@ var getActivityAction = {
|
|
|
1026
1417
|
}
|
|
1027
1418
|
]
|
|
1028
1419
|
],
|
|
1029
|
-
validate: async (runtime, message, state
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
1034
|
-
const __avRegex = /\b(?:get|linear|activity)\b/i;
|
|
1035
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
1036
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
1037
|
-
const __avExpectedSource = "";
|
|
1038
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
1039
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
1040
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
1041
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
1042
|
-
return false;
|
|
1043
|
-
}
|
|
1044
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
1045
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
1046
|
-
return !!apiKey;
|
|
1047
|
-
};
|
|
1048
|
-
try {
|
|
1049
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
1050
|
-
} catch {
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
|
1053
|
-
},
|
|
1420
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
1421
|
+
keywords: ["get", "linear", "activity"],
|
|
1422
|
+
regexAlternation: "get|linear|activity"
|
|
1423
|
+
}),
|
|
1054
1424
|
async handler(runtime, message, _state, _options, callback) {
|
|
1055
1425
|
try {
|
|
1056
1426
|
const linearService = runtime.getService("linear");
|
|
1057
1427
|
if (!linearService) {
|
|
1058
1428
|
throw new Error("Linear service not available");
|
|
1059
1429
|
}
|
|
1430
|
+
const accountId = getLinearAccountId(runtime, _options);
|
|
1060
1431
|
const content = message.content.text || "";
|
|
1061
|
-
const
|
|
1062
|
-
|
|
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;
|
|
1063
1435
|
if (content) {
|
|
1064
1436
|
const prompt = getActivityTemplate.replace("{{userMessage}}", content);
|
|
1065
1437
|
const response = await runtime.useModel(ModelType4.TEXT_LARGE, {
|
|
@@ -1067,14 +1439,20 @@ var getActivityAction = {
|
|
|
1067
1439
|
});
|
|
1068
1440
|
if (response) {
|
|
1069
1441
|
try {
|
|
1070
|
-
const parsed =
|
|
1071
|
-
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) {
|
|
1072
1448
|
const now = new Date;
|
|
1073
1449
|
let fromDate;
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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) {
|
|
1078
1456
|
case "today":
|
|
1079
1457
|
fromDate = new Date(now.setHours(0, 0, 0, 0));
|
|
1080
1458
|
break;
|
|
@@ -1099,25 +1477,29 @@ var getActivityAction = {
|
|
|
1099
1477
|
filters.fromDate = fromDate.toISOString();
|
|
1100
1478
|
}
|
|
1101
1479
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1480
|
+
const actionTypes = getStringArrayValue(parsed.actionTypes);
|
|
1481
|
+
if (actionTypes && actionTypes.length > 0) {
|
|
1482
|
+
filters.action = actionTypes[0];
|
|
1104
1483
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1484
|
+
const resourceTypes = getStringArrayValue(parsed.resourceTypes);
|
|
1485
|
+
if (resourceTypes && resourceTypes.length > 0) {
|
|
1486
|
+
filters.resource_type = resourceTypes[0];
|
|
1107
1487
|
}
|
|
1108
|
-
|
|
1109
|
-
|
|
1488
|
+
const resourceId = getStringValue(parsed.resourceId);
|
|
1489
|
+
if (resourceId) {
|
|
1490
|
+
filters.resource_id = resourceId;
|
|
1110
1491
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1492
|
+
const successFilter = getStringValue(parsed.successFilter);
|
|
1493
|
+
if (successFilter && successFilter !== "all") {
|
|
1494
|
+
filters.success = successFilter === "success";
|
|
1113
1495
|
}
|
|
1114
|
-
limit = parsed.limit || 10;
|
|
1496
|
+
limit = getNumberValue(parsed.limit) || 10;
|
|
1115
1497
|
} catch (parseError) {
|
|
1116
|
-
|
|
1498
|
+
logger6.warn("Failed to parse activity filters:", parseError);
|
|
1117
1499
|
}
|
|
1118
1500
|
}
|
|
1119
1501
|
}
|
|
1120
|
-
let activity = linearService.getActivityLog(limit * 2, filters);
|
|
1502
|
+
let activity = linearService.getActivityLog(limit * 2, filters, accountId);
|
|
1121
1503
|
if (filters.fromDate) {
|
|
1122
1504
|
const fromDateValue = filters.fromDate;
|
|
1123
1505
|
const fromDate = typeof fromDateValue === "string" ? fromDateValue : fromDateValue instanceof Date ? fromDateValue.toISOString() : String(fromDateValue);
|
|
@@ -1137,14 +1519,15 @@ var getActivityAction = {
|
|
|
1137
1519
|
text: noActivityMessage,
|
|
1138
1520
|
success: true,
|
|
1139
1521
|
data: {
|
|
1140
|
-
activity: []
|
|
1522
|
+
activity: [],
|
|
1523
|
+
accountId
|
|
1141
1524
|
}
|
|
1142
1525
|
};
|
|
1143
1526
|
}
|
|
1144
1527
|
const activityText = activity.map((item, index) => {
|
|
1145
1528
|
const time = new Date(item.timestamp).toLocaleString();
|
|
1146
1529
|
const status = item.success ? "✅" : "❌";
|
|
1147
|
-
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(", ");
|
|
1148
1531
|
return `${index + 1}. ${status} ${item.action} on ${item.resource_type} ${item.resource_id}
|
|
1149
1532
|
Time: ${time}
|
|
1150
1533
|
${details ? `Details: ${details}` : ""}${item.error ? `
|
|
@@ -1171,18 +1554,19 @@ ${activityText}`;
|
|
|
1171
1554
|
resource_id: item.resource_id,
|
|
1172
1555
|
success: item.success,
|
|
1173
1556
|
error: item.error,
|
|
1174
|
-
details:
|
|
1557
|
+
details: formatActivityDetail(item.details),
|
|
1175
1558
|
timestamp: typeof item.timestamp === "string" ? item.timestamp : new Date(item.timestamp).toISOString()
|
|
1176
1559
|
})),
|
|
1177
1560
|
filters: filters ? {
|
|
1178
1561
|
...filters,
|
|
1179
1562
|
fromDate: filters.fromDate ? typeof filters.fromDate === "string" ? filters.fromDate : String(filters.fromDate) : undefined
|
|
1180
1563
|
} : undefined,
|
|
1181
|
-
count: activity.length
|
|
1564
|
+
count: activity.length,
|
|
1565
|
+
accountId
|
|
1182
1566
|
}
|
|
1183
1567
|
};
|
|
1184
1568
|
} catch (error) {
|
|
1185
|
-
|
|
1569
|
+
logger6.error("Failed to get activity:", error);
|
|
1186
1570
|
const errorMessage = `❌ Failed to get activity: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
1187
1571
|
await callback?.({
|
|
1188
1572
|
text: errorMessage,
|
|
@@ -1198,12 +1582,16 @@ ${activityText}`;
|
|
|
1198
1582
|
|
|
1199
1583
|
// src/actions/getIssue.ts
|
|
1200
1584
|
import {
|
|
1201
|
-
logger as
|
|
1585
|
+
logger as logger7,
|
|
1202
1586
|
ModelType as ModelType5
|
|
1203
1587
|
} from "@elizaos/core";
|
|
1204
1588
|
var getIssueAction = {
|
|
1205
1589
|
name: "GET_LINEAR_ISSUE",
|
|
1590
|
+
contexts: ["tasks", "connectors", "knowledge"],
|
|
1591
|
+
contextGate: { anyOf: ["tasks", "connectors", "knowledge"] },
|
|
1592
|
+
roleGate: { minRole: "USER" },
|
|
1206
1593
|
description: "Get details of a specific Linear issue",
|
|
1594
|
+
descriptionCompressed: "get detail specific Linear issue",
|
|
1207
1595
|
similes: [
|
|
1208
1596
|
"get-linear-issue",
|
|
1209
1597
|
"show-linear-issue",
|
|
@@ -1211,6 +1599,21 @@ var getIssueAction = {
|
|
|
1211
1599
|
"check-linear-issue",
|
|
1212
1600
|
"find-linear-issue"
|
|
1213
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
|
+
],
|
|
1214
1617
|
examples: [
|
|
1215
1618
|
[
|
|
1216
1619
|
{
|
|
@@ -1258,38 +1661,19 @@ var getIssueAction = {
|
|
|
1258
1661
|
}
|
|
1259
1662
|
]
|
|
1260
1663
|
],
|
|
1261
|
-
validate: async (runtime, message, state
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
1266
|
-
const __avRegex = /\b(?:get|linear|issue)\b/i;
|
|
1267
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
1268
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
1269
|
-
const __avExpectedSource = "";
|
|
1270
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
1271
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
1272
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
1273
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
1274
|
-
return false;
|
|
1275
|
-
}
|
|
1276
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
1277
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
1278
|
-
return !!apiKey;
|
|
1279
|
-
};
|
|
1280
|
-
try {
|
|
1281
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
1282
|
-
} catch {
|
|
1283
|
-
return false;
|
|
1284
|
-
}
|
|
1285
|
-
},
|
|
1664
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
1665
|
+
keywords: ["get", "linear", "issue"],
|
|
1666
|
+
regexAlternation: "get|linear|issue"
|
|
1667
|
+
}),
|
|
1286
1668
|
async handler(runtime, message, _state, _options, callback) {
|
|
1287
1669
|
try {
|
|
1288
1670
|
const linearService = runtime.getService("linear");
|
|
1289
1671
|
if (!linearService) {
|
|
1290
1672
|
throw new Error("Linear service not available");
|
|
1291
1673
|
}
|
|
1292
|
-
const
|
|
1674
|
+
const accountId = getLinearAccountId(runtime, _options);
|
|
1675
|
+
const params = _options?.parameters ?? {};
|
|
1676
|
+
const content = params.query ?? params.issueId ?? message.content.text;
|
|
1293
1677
|
if (!content) {
|
|
1294
1678
|
const errorMessage2 = "Please specify which issue you want to see.";
|
|
1295
1679
|
await callback?.({
|
|
@@ -1301,6 +1685,10 @@ var getIssueAction = {
|
|
|
1301
1685
|
success: false
|
|
1302
1686
|
};
|
|
1303
1687
|
}
|
|
1688
|
+
if (params.issueId) {
|
|
1689
|
+
const issue = await linearService.getIssue(params.issueId, accountId);
|
|
1690
|
+
return await formatIssueResponse(issue, callback, message);
|
|
1691
|
+
}
|
|
1304
1692
|
const prompt = getIssueTemplate.replace("{{userMessage}}", content);
|
|
1305
1693
|
const response = await runtime.useModel(ModelType5.TEXT_LARGE, {
|
|
1306
1694
|
prompt
|
|
@@ -1308,26 +1696,34 @@ var getIssueAction = {
|
|
|
1308
1696
|
if (!response) {
|
|
1309
1697
|
const issueMatch = content.match(/(\w+-\d+)/);
|
|
1310
1698
|
if (issueMatch) {
|
|
1311
|
-
const issue = await linearService.getIssue(issueMatch[1]);
|
|
1699
|
+
const issue = await linearService.getIssue(issueMatch[1], accountId);
|
|
1312
1700
|
return await formatIssueResponse(issue, callback, message);
|
|
1313
1701
|
}
|
|
1314
1702
|
throw new Error("Could not understand issue reference");
|
|
1315
1703
|
}
|
|
1316
1704
|
try {
|
|
1317
|
-
const parsed =
|
|
1318
|
-
if (parsed.
|
|
1319
|
-
|
|
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);
|
|
1320
1712
|
return await formatIssueResponse(issue, callback, message);
|
|
1321
1713
|
}
|
|
1322
|
-
|
|
1714
|
+
const searchBy = getRecordValue(parsed.searchBy);
|
|
1715
|
+
if (searchBy && Object.keys(searchBy).length > 0) {
|
|
1323
1716
|
const filters = {};
|
|
1324
|
-
|
|
1325
|
-
|
|
1717
|
+
const title = getStringValue(searchBy.title);
|
|
1718
|
+
if (title) {
|
|
1719
|
+
filters.query = title;
|
|
1326
1720
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1721
|
+
const assignee = getStringValue(searchBy.assignee);
|
|
1722
|
+
if (assignee) {
|
|
1723
|
+
filters.assignee = [assignee];
|
|
1329
1724
|
}
|
|
1330
|
-
|
|
1725
|
+
const priorityValue = getStringValue(searchBy.priority);
|
|
1726
|
+
if (priorityValue) {
|
|
1331
1727
|
const priorityMap = {
|
|
1332
1728
|
urgent: 1,
|
|
1333
1729
|
high: 2,
|
|
@@ -1338,25 +1734,27 @@ var getIssueAction = {
|
|
|
1338
1734
|
"3": 3,
|
|
1339
1735
|
"4": 4
|
|
1340
1736
|
};
|
|
1341
|
-
const priority = priorityMap[
|
|
1737
|
+
const priority = priorityMap[priorityValue.toLowerCase()];
|
|
1342
1738
|
if (priority) {
|
|
1343
1739
|
filters.priority = [priority];
|
|
1344
1740
|
}
|
|
1345
1741
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1742
|
+
const team = getStringValue(searchBy.team);
|
|
1743
|
+
if (team) {
|
|
1744
|
+
filters.team = team;
|
|
1348
1745
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1746
|
+
const state = getStringValue(searchBy.state);
|
|
1747
|
+
if (state) {
|
|
1748
|
+
filters.state = [state];
|
|
1351
1749
|
}
|
|
1352
|
-
const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
1750
|
+
const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
1353
1751
|
if (defaultTeamKey && !filters.team) {
|
|
1354
1752
|
filters.team = defaultTeamKey;
|
|
1355
1753
|
}
|
|
1356
1754
|
const issues = await linearService.searchIssues({
|
|
1357
1755
|
...filters,
|
|
1358
|
-
limit:
|
|
1359
|
-
});
|
|
1756
|
+
limit: getStringValue(searchBy.recency) ? 10 : 5
|
|
1757
|
+
}, accountId);
|
|
1360
1758
|
if (issues.length === 0) {
|
|
1361
1759
|
const noResultsMessage = "No issues found matching your criteria.";
|
|
1362
1760
|
await callback?.({
|
|
@@ -1368,18 +1766,18 @@ var getIssueAction = {
|
|
|
1368
1766
|
success: false
|
|
1369
1767
|
};
|
|
1370
1768
|
}
|
|
1371
|
-
if (
|
|
1769
|
+
if (getStringValue(searchBy.recency)) {
|
|
1372
1770
|
issues.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1373
1771
|
}
|
|
1374
|
-
if (
|
|
1772
|
+
if (getStringValue(searchBy.recency) && issues.length > 0) {
|
|
1375
1773
|
return await formatIssueResponse(issues[0], callback, message);
|
|
1376
1774
|
}
|
|
1377
1775
|
if (issues.length === 1) {
|
|
1378
1776
|
return await formatIssueResponse(issues[0], callback, message);
|
|
1379
1777
|
}
|
|
1380
1778
|
const issueList = await Promise.all(issues.slice(0, 5).map(async (issue, index) => {
|
|
1381
|
-
const
|
|
1382
|
-
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"})`;
|
|
1383
1781
|
}));
|
|
1384
1782
|
const clarifyMessage = `Found ${issues.length} issues matching your criteria:
|
|
1385
1783
|
${issueList.join(`
|
|
@@ -1404,10 +1802,10 @@ Please specify which one you want to see by its ID.`;
|
|
|
1404
1802
|
};
|
|
1405
1803
|
}
|
|
1406
1804
|
} catch (parseError) {
|
|
1407
|
-
|
|
1805
|
+
logger7.warn("Failed to parse LLM response, falling back to regex:", parseError);
|
|
1408
1806
|
const issueMatch = content.match(/(\w+-\d+)/);
|
|
1409
1807
|
if (issueMatch) {
|
|
1410
|
-
const issue = await linearService.getIssue(issueMatch[1]);
|
|
1808
|
+
const issue = await linearService.getIssue(issueMatch[1], accountId);
|
|
1411
1809
|
return await formatIssueResponse(issue, callback, message);
|
|
1412
1810
|
}
|
|
1413
1811
|
}
|
|
@@ -1421,7 +1819,7 @@ Please specify which one you want to see by its ID.`;
|
|
|
1421
1819
|
success: false
|
|
1422
1820
|
};
|
|
1423
1821
|
} catch (error) {
|
|
1424
|
-
|
|
1822
|
+
logger7.error("Failed to get issue:", error);
|
|
1425
1823
|
const errorMessage = `❌ Failed to get issue: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
1426
1824
|
await callback?.({
|
|
1427
1825
|
text: errorMessage,
|
|
@@ -1512,489 +1910,148 @@ View in Linear: ${issue.url}`;
|
|
|
1512
1910
|
};
|
|
1513
1911
|
}
|
|
1514
1912
|
|
|
1515
|
-
// src/actions/
|
|
1913
|
+
// src/actions/listComments.ts
|
|
1516
1914
|
import {
|
|
1517
|
-
logger as
|
|
1518
|
-
ModelType as ModelType6
|
|
1915
|
+
logger as logger8
|
|
1519
1916
|
} from "@elizaos/core";
|
|
1520
|
-
var
|
|
1521
|
-
name: "
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
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
|
|
1528
1938
|
],
|
|
1939
|
+
similes: ["get-linear-comments", "show-linear-comments", "fetch-linear-comments"],
|
|
1529
1940
|
examples: [
|
|
1530
1941
|
[
|
|
1531
1942
|
{
|
|
1532
1943
|
name: "User",
|
|
1533
|
-
content: {
|
|
1534
|
-
text: "Show me all projects"
|
|
1535
|
-
}
|
|
1536
|
-
},
|
|
1537
|
-
{
|
|
1538
|
-
name: "Assistant",
|
|
1539
|
-
content: {
|
|
1540
|
-
text: "I'll list all the projects in Linear for you.",
|
|
1541
|
-
actions: ["LIST_LINEAR_PROJECTS"]
|
|
1542
|
-
}
|
|
1543
|
-
}
|
|
1544
|
-
],
|
|
1545
|
-
[
|
|
1546
|
-
{
|
|
1547
|
-
name: "User",
|
|
1548
|
-
content: {
|
|
1549
|
-
text: "What active projects do we have?"
|
|
1550
|
-
}
|
|
1551
|
-
},
|
|
1552
|
-
{
|
|
1553
|
-
name: "Assistant",
|
|
1554
|
-
content: {
|
|
1555
|
-
text: "Let me show you all the active projects.",
|
|
1556
|
-
actions: ["LIST_LINEAR_PROJECTS"]
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
],
|
|
1560
|
-
[
|
|
1561
|
-
{
|
|
1562
|
-
name: "User",
|
|
1563
|
-
content: {
|
|
1564
|
-
text: "Show me projects for the engineering team"
|
|
1565
|
-
}
|
|
1944
|
+
content: { text: "Show the comments on ENG-123." }
|
|
1566
1945
|
},
|
|
1567
1946
|
{
|
|
1568
1947
|
name: "Assistant",
|
|
1569
1948
|
content: {
|
|
1570
|
-
text: "
|
|
1571
|
-
actions: ["
|
|
1949
|
+
text: "Here are the comments on ENG-123.",
|
|
1950
|
+
actions: ["LIST_LINEAR_COMMENTS"]
|
|
1572
1951
|
}
|
|
1573
1952
|
}
|
|
1574
1953
|
]
|
|
1575
1954
|
],
|
|
1576
|
-
validate: async (runtime, message, state
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
1581
|
-
const __avRegex = /\b(?:list|linear|projects)\b/i;
|
|
1582
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
1583
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
1584
|
-
const __avExpectedSource = "";
|
|
1585
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
1586
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
1587
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
1588
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
1589
|
-
return false;
|
|
1590
|
-
}
|
|
1591
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
1592
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
1593
|
-
return !!apiKey;
|
|
1594
|
-
};
|
|
1595
|
-
try {
|
|
1596
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
1597
|
-
} catch {
|
|
1598
|
-
return false;
|
|
1599
|
-
}
|
|
1600
|
-
},
|
|
1955
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
1956
|
+
keywords: ["list", "comments", "linear", "issue"],
|
|
1957
|
+
regexAlternation: "list|comments|linear|issue"
|
|
1958
|
+
}),
|
|
1601
1959
|
async handler(runtime, message, _state, _options, callback) {
|
|
1602
1960
|
try {
|
|
1603
1961
|
const linearService = runtime.getService("linear");
|
|
1604
1962
|
if (!linearService) {
|
|
1605
1963
|
throw new Error("Linear service not available");
|
|
1606
1964
|
}
|
|
1607
|
-
const
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
if (
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
const teams = await linearService.getTeams();
|
|
1621
|
-
const team = teams.find((t) => t.key.toLowerCase() === parsed.teamFilter.toLowerCase() || t.name.toLowerCase() === parsed.teamFilter.toLowerCase());
|
|
1622
|
-
if (team) {
|
|
1623
|
-
teamId = team.id;
|
|
1624
|
-
logger7.info(`Filtering projects by team: ${team.name} (${team.key})`);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
showAll = parsed.showAll === true;
|
|
1628
|
-
stateFilter = parsed.stateFilter;
|
|
1629
|
-
} catch (parseError) {
|
|
1630
|
-
logger7.warn("Failed to parse project filters, using basic parsing:", parseError);
|
|
1631
|
-
const teamMatch = content.match(/(?:for|in|of)\s+(?:the\s+)?(\w+)\s+team/i);
|
|
1632
|
-
if (teamMatch) {
|
|
1633
|
-
const teams = await linearService.getTeams();
|
|
1634
|
-
const team = teams.find((t) => t.key.toLowerCase() === teamMatch[1].toLowerCase() || t.name.toLowerCase() === teamMatch[1].toLowerCase());
|
|
1635
|
-
if (team) {
|
|
1636
|
-
teamId = team.id;
|
|
1637
|
-
logger7.info(`Filtering projects by team: ${team.name} (${team.key})`);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
showAll = content.toLowerCase().includes("all") && content.toLowerCase().includes("project");
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
if (!teamId && !showAll) {
|
|
1645
|
-
const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
1646
|
-
if (defaultTeamKey) {
|
|
1647
|
-
const teams = await linearService.getTeams();
|
|
1648
|
-
const defaultTeam = teams.find((t) => t.key.toLowerCase() === defaultTeamKey.toLowerCase());
|
|
1649
|
-
if (defaultTeam) {
|
|
1650
|
-
teamId = defaultTeam.id;
|
|
1651
|
-
logger7.info(`Applying default team filter for projects: ${defaultTeam.name} (${defaultTeam.key})`);
|
|
1652
|
-
}
|
|
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 };
|
|
1653
1978
|
}
|
|
1979
|
+
const extractedId = match[1];
|
|
1980
|
+
const comments2 = await linearService.listComments(extractedId, limit, accountId);
|
|
1981
|
+
return formatCommentResult(extractedId, comments2, message, callback);
|
|
1654
1982
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
projects = projects.filter((project) => {
|
|
1658
|
-
const state = project.state?.toLowerCase() || "";
|
|
1659
|
-
if (stateFilter === "active") {
|
|
1660
|
-
return state === "started" || state === "in progress" || !state;
|
|
1661
|
-
} else if (stateFilter === "planned") {
|
|
1662
|
-
return state === "planned" || state === "backlog";
|
|
1663
|
-
} else if (stateFilter === "completed") {
|
|
1664
|
-
return state === "completed" || state === "done" || state === "canceled";
|
|
1665
|
-
}
|
|
1666
|
-
return true;
|
|
1667
|
-
});
|
|
1668
|
-
}
|
|
1669
|
-
if (projects.length === 0) {
|
|
1670
|
-
const noProjectsMessage = teamId ? "No projects found for the specified team." : "No projects found in Linear.";
|
|
1671
|
-
await callback?.({
|
|
1672
|
-
text: noProjectsMessage,
|
|
1673
|
-
source: message.content.source
|
|
1674
|
-
});
|
|
1675
|
-
return {
|
|
1676
|
-
text: noProjectsMessage,
|
|
1677
|
-
success: true,
|
|
1678
|
-
data: {
|
|
1679
|
-
projects: []
|
|
1680
|
-
}
|
|
1681
|
-
};
|
|
1682
|
-
}
|
|
1683
|
-
const projectsWithDetails = await Promise.all(projects.map(async (project) => {
|
|
1684
|
-
const teamsQuery = await project.teams();
|
|
1685
|
-
const teams = await teamsQuery.nodes;
|
|
1686
|
-
const lead = await project.lead;
|
|
1687
|
-
return {
|
|
1688
|
-
...project,
|
|
1689
|
-
teamsList: teams,
|
|
1690
|
-
leadUser: lead
|
|
1691
|
-
};
|
|
1692
|
-
}));
|
|
1693
|
-
const projectList = projectsWithDetails.map((project, index) => {
|
|
1694
|
-
const teamNames = project.teamsList.map((t) => t.name).join(", ") || "No teams";
|
|
1695
|
-
const status = project.state || "Active";
|
|
1696
|
-
const progress = project.progress ? ` (${Math.round(project.progress * 100)}% complete)` : "";
|
|
1697
|
-
const lead = project.leadUser ? ` | Lead: ${project.leadUser.name}` : "";
|
|
1698
|
-
const dates = [];
|
|
1699
|
-
if (project.startDate)
|
|
1700
|
-
dates.push(`Start: ${new Date(project.startDate).toLocaleDateString()}`);
|
|
1701
|
-
if (project.targetDate)
|
|
1702
|
-
dates.push(`Due: ${new Date(project.targetDate).toLocaleDateString()}`);
|
|
1703
|
-
const dateInfo = dates.length > 0 ? `
|
|
1704
|
-
${dates.join(" | ")}` : "";
|
|
1705
|
-
return `${index + 1}. ${project.name}${project.description ? ` - ${project.description}` : ""}
|
|
1706
|
-
Status: ${status}${progress} | Teams: ${teamNames}${lead}${dateInfo}`;
|
|
1707
|
-
}).join(`
|
|
1708
|
-
|
|
1709
|
-
`);
|
|
1710
|
-
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"}:`;
|
|
1711
|
-
const resultMessage = `${headerText}
|
|
1712
|
-
|
|
1713
|
-
${projectList}`;
|
|
1714
|
-
await callback?.({
|
|
1715
|
-
text: resultMessage,
|
|
1716
|
-
source: message.content.source
|
|
1717
|
-
});
|
|
1718
|
-
return {
|
|
1719
|
-
text: `Found ${projects.length} project${projects.length === 1 ? "" : "s"}`,
|
|
1720
|
-
success: true,
|
|
1721
|
-
data: {
|
|
1722
|
-
projects: projectsWithDetails.map((p) => ({
|
|
1723
|
-
id: p.id,
|
|
1724
|
-
name: p.name,
|
|
1725
|
-
description: p.description,
|
|
1726
|
-
url: p.url,
|
|
1727
|
-
teams: p.teamsList.map((t) => ({
|
|
1728
|
-
id: t.id,
|
|
1729
|
-
name: t.name,
|
|
1730
|
-
key: t.key
|
|
1731
|
-
})),
|
|
1732
|
-
lead: p.leadUser ? {
|
|
1733
|
-
id: p.leadUser.id,
|
|
1734
|
-
name: p.leadUser.name,
|
|
1735
|
-
email: p.leadUser.email
|
|
1736
|
-
} : null,
|
|
1737
|
-
state: p.state,
|
|
1738
|
-
progress: p.progress,
|
|
1739
|
-
startDate: p.startDate,
|
|
1740
|
-
targetDate: p.targetDate
|
|
1741
|
-
})),
|
|
1742
|
-
count: projects.length,
|
|
1743
|
-
filters: {
|
|
1744
|
-
team: teamId,
|
|
1745
|
-
state: stateFilter
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
};
|
|
1983
|
+
const comments = await linearService.listComments(issueId, limit, accountId);
|
|
1984
|
+
return formatCommentResult(issueId, comments, message, callback);
|
|
1749
1985
|
} catch (error) {
|
|
1750
|
-
|
|
1751
|
-
const errorMessage =
|
|
1752
|
-
await callback?.({
|
|
1753
|
-
|
|
1754
|
-
source: message.content.source
|
|
1755
|
-
});
|
|
1756
|
-
return {
|
|
1757
|
-
text: errorMessage,
|
|
1758
|
-
success: false
|
|
1759
|
-
};
|
|
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 };
|
|
1760
1990
|
}
|
|
1761
1991
|
}
|
|
1762
1992
|
};
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
} from "@elizaos/core";
|
|
1769
|
-
var listTeamsAction = {
|
|
1770
|
-
name: "LIST_LINEAR_TEAMS",
|
|
1771
|
-
description: "List teams in Linear with optional filters",
|
|
1772
|
-
similes: ["list-linear-teams", "show-linear-teams", "get-linear-teams", "view-linear-teams"],
|
|
1773
|
-
examples: [
|
|
1774
|
-
[
|
|
1775
|
-
{
|
|
1776
|
-
name: "User",
|
|
1777
|
-
content: {
|
|
1778
|
-
text: "Show me all teams"
|
|
1779
|
-
}
|
|
1780
|
-
},
|
|
1781
|
-
{
|
|
1782
|
-
name: "Assistant",
|
|
1783
|
-
content: {
|
|
1784
|
-
text: "I'll list all the teams in Linear for you.",
|
|
1785
|
-
actions: ["LIST_LINEAR_TEAMS"]
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
],
|
|
1789
|
-
[
|
|
1790
|
-
{
|
|
1791
|
-
name: "User",
|
|
1792
|
-
content: {
|
|
1793
|
-
text: "Which engineering teams do we have?"
|
|
1794
|
-
}
|
|
1795
|
-
},
|
|
1796
|
-
{
|
|
1797
|
-
name: "Assistant",
|
|
1798
|
-
content: {
|
|
1799
|
-
text: "Let me find the engineering teams for you.",
|
|
1800
|
-
actions: ["LIST_LINEAR_TEAMS"]
|
|
1801
|
-
}
|
|
1802
|
-
}
|
|
1803
|
-
],
|
|
1804
|
-
[
|
|
1805
|
-
{
|
|
1806
|
-
name: "User",
|
|
1807
|
-
content: {
|
|
1808
|
-
text: "Show me the teams I'm part of"
|
|
1809
|
-
}
|
|
1810
|
-
},
|
|
1811
|
-
{
|
|
1812
|
-
name: "Assistant",
|
|
1813
|
-
content: {
|
|
1814
|
-
text: "I'll show you the teams you're a member of.",
|
|
1815
|
-
actions: ["LIST_LINEAR_TEAMS"]
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
]
|
|
1819
|
-
],
|
|
1820
|
-
validate: async (runtime, message, state, options) => {
|
|
1821
|
-
const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
|
|
1822
|
-
const __avText = __avTextRaw.toLowerCase();
|
|
1823
|
-
const __avKeywords = ["list", "linear", "teams"];
|
|
1824
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
1825
|
-
const __avRegex = /\b(?:list|linear|teams)\b/i;
|
|
1826
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
1827
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
1828
|
-
const __avExpectedSource = "";
|
|
1829
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
1830
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
1831
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
1832
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
1833
|
-
return false;
|
|
1834
|
-
}
|
|
1835
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
1836
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
1837
|
-
return !!apiKey;
|
|
1838
|
-
};
|
|
1839
|
-
try {
|
|
1840
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
1841
|
-
} catch {
|
|
1842
|
-
return false;
|
|
1843
|
-
}
|
|
1844
|
-
},
|
|
1845
|
-
async handler(runtime, message, _state, _options, callback) {
|
|
1846
|
-
try {
|
|
1847
|
-
const linearService = runtime.getService("linear");
|
|
1848
|
-
if (!linearService) {
|
|
1849
|
-
throw new Error("Linear service not available");
|
|
1850
|
-
}
|
|
1851
|
-
const content = message.content.text || "";
|
|
1852
|
-
let nameFilter;
|
|
1853
|
-
let specificTeam;
|
|
1854
|
-
let myTeams = false;
|
|
1855
|
-
let includeDetails = false;
|
|
1856
|
-
if (content) {
|
|
1857
|
-
const prompt = listTeamsTemplate.replace("{{userMessage}}", content);
|
|
1858
|
-
const response = await runtime.useModel(ModelType7.TEXT_LARGE, {
|
|
1859
|
-
prompt
|
|
1860
|
-
});
|
|
1861
|
-
if (response) {
|
|
1862
|
-
try {
|
|
1863
|
-
const parsed = JSON.parse(response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim());
|
|
1864
|
-
nameFilter = parsed.nameFilter;
|
|
1865
|
-
specificTeam = parsed.specificTeam;
|
|
1866
|
-
myTeams = parsed.myTeams === true;
|
|
1867
|
-
includeDetails = parsed.includeDetails === true;
|
|
1868
|
-
} catch (parseError) {
|
|
1869
|
-
logger8.warn("Failed to parse team filters:", parseError);
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
let teams = await linearService.getTeams();
|
|
1874
|
-
if (specificTeam) {
|
|
1875
|
-
teams = teams.filter((team) => team.key.toLowerCase() === specificTeam.toLowerCase() || team.name.toLowerCase() === specificTeam.toLowerCase());
|
|
1876
|
-
}
|
|
1877
|
-
if (nameFilter && !specificTeam) {
|
|
1878
|
-
const keywords = nameFilter.toLowerCase().split(/\s+/);
|
|
1879
|
-
teams = teams.filter((team) => {
|
|
1880
|
-
const teamText = `${team.name} ${team.description || ""}`.toLowerCase();
|
|
1881
|
-
return keywords.some((keyword) => teamText.includes(keyword));
|
|
1882
|
-
});
|
|
1883
|
-
}
|
|
1884
|
-
if (myTeams) {
|
|
1885
|
-
try {
|
|
1886
|
-
const userTeams = await linearService.getUserTeams();
|
|
1887
|
-
const userTeamIds = new Set(userTeams.map((t) => t.id));
|
|
1888
|
-
teams = teams.filter((team) => userTeamIds.has(team.id));
|
|
1889
|
-
} catch (error) {
|
|
1890
|
-
logger8.warn("Could not filter for user's teams:", error);
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
if (teams.length === 0) {
|
|
1894
|
-
const noTeamsMessage = specificTeam ? `No team found matching "${specificTeam}".` : nameFilter ? `No teams found matching "${nameFilter}".` : "No teams found in Linear.";
|
|
1895
|
-
await callback?.({
|
|
1896
|
-
text: noTeamsMessage,
|
|
1897
|
-
source: message.content.source
|
|
1898
|
-
});
|
|
1899
|
-
return {
|
|
1900
|
-
text: noTeamsMessage,
|
|
1901
|
-
success: true,
|
|
1902
|
-
data: {
|
|
1903
|
-
teams: []
|
|
1904
|
-
}
|
|
1905
|
-
};
|
|
1906
|
-
}
|
|
1907
|
-
let teamsWithDetails = teams;
|
|
1908
|
-
if (includeDetails || specificTeam) {
|
|
1909
|
-
teamsWithDetails = await Promise.all(teams.map(async (team) => {
|
|
1910
|
-
const membersQuery = await team.members();
|
|
1911
|
-
const members = await membersQuery.nodes;
|
|
1912
|
-
const projectsQuery = await team.projects();
|
|
1913
|
-
const projects = await projectsQuery.nodes;
|
|
1914
|
-
return Object.assign(team, {
|
|
1915
|
-
memberCount: members.length,
|
|
1916
|
-
projectCount: projects.length,
|
|
1917
|
-
membersList: specificTeam ? members.slice(0, 5) : []
|
|
1918
|
-
});
|
|
1919
|
-
}));
|
|
1920
|
-
}
|
|
1921
|
-
const teamList = teamsWithDetails.map((team, index) => {
|
|
1922
|
-
let info = `${index + 1}. ${team.name} (${team.key})`;
|
|
1923
|
-
if (team.description) {
|
|
1924
|
-
info += `
|
|
1925
|
-
${team.description}`;
|
|
1926
|
-
}
|
|
1927
|
-
if (includeDetails || specificTeam) {
|
|
1928
|
-
info += `
|
|
1929
|
-
Members: ${team.memberCount ?? 0} | Projects: ${team.projectCount ?? 0}`;
|
|
1930
|
-
const membersList = team.membersList ?? [];
|
|
1931
|
-
if (specificTeam && membersList.length > 0) {
|
|
1932
|
-
const memberNames = membersList.map((m) => m.name).join(", ");
|
|
1933
|
-
info += `
|
|
1934
|
-
Team members: ${memberNames}${(team.memberCount ?? 0) > 5 ? " ..." : ""}`;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
return info;
|
|
1938
|
-
}).join(`
|
|
1939
|
-
|
|
1940
|
-
`);
|
|
1941
|
-
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"}:`;
|
|
1942
|
-
const resultMessage = `${headerText}
|
|
1943
|
-
|
|
1944
|
-
${teamList}`;
|
|
1945
|
-
await callback?.({
|
|
1946
|
-
text: resultMessage,
|
|
1947
|
-
source: message.content.source
|
|
1948
|
-
});
|
|
1949
|
-
return {
|
|
1950
|
-
text: `Found ${teams.length} team${teams.length === 1 ? "" : "s"}`,
|
|
1951
|
-
success: true,
|
|
1952
|
-
data: {
|
|
1953
|
-
teams: teamsWithDetails.map((t) => ({
|
|
1954
|
-
id: t.id,
|
|
1955
|
-
name: t.name,
|
|
1956
|
-
key: t.key,
|
|
1957
|
-
description: t.description,
|
|
1958
|
-
memberCount: t.memberCount,
|
|
1959
|
-
projectCount: t.projectCount
|
|
1960
|
-
})),
|
|
1961
|
-
count: teams.length,
|
|
1962
|
-
filters: {
|
|
1963
|
-
name: nameFilter,
|
|
1964
|
-
specific: specificTeam
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
};
|
|
1968
|
-
} catch (error) {
|
|
1969
|
-
logger8.error("Failed to list teams:", error);
|
|
1970
|
-
const errorMessage = `❌ Failed to list teams: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
1971
|
-
await callback?.({
|
|
1972
|
-
text: errorMessage,
|
|
1973
|
-
source: message.content.source
|
|
1974
|
-
});
|
|
1975
|
-
return {
|
|
1976
|
-
text: errorMessage,
|
|
1977
|
-
success: false
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
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: [] } };
|
|
1980
1998
|
}
|
|
1981
|
-
|
|
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
|
+
}
|
|
1982
2020
|
|
|
1983
2021
|
// src/actions/searchIssues.ts
|
|
1984
2022
|
import {
|
|
1985
2023
|
logger as logger9,
|
|
1986
|
-
ModelType as
|
|
2024
|
+
ModelType as ModelType6
|
|
1987
2025
|
} from "@elizaos/core";
|
|
1988
2026
|
var searchTemplate = searchIssuesTemplate;
|
|
1989
2027
|
var searchIssuesAction = {
|
|
1990
2028
|
name: "SEARCH_LINEAR_ISSUES",
|
|
2029
|
+
contexts: ["tasks", "connectors", "knowledge"],
|
|
2030
|
+
contextGate: { anyOf: ["tasks", "connectors", "knowledge"] },
|
|
2031
|
+
roleGate: { minRole: "USER" },
|
|
1991
2032
|
description: "Search for issues in Linear with various filters",
|
|
2033
|
+
descriptionCompressed: "search issue Linear w/ various filter",
|
|
1992
2034
|
similes: [
|
|
1993
2035
|
"search-linear-issues",
|
|
1994
2036
|
"find-linear-issues",
|
|
1995
2037
|
"query-linear-issues",
|
|
1996
2038
|
"list-linear-issues"
|
|
1997
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
|
+
],
|
|
1998
2055
|
examples: [
|
|
1999
2056
|
[
|
|
2000
2057
|
{
|
|
@@ -2042,37 +2099,17 @@ var searchIssuesAction = {
|
|
|
2042
2099
|
}
|
|
2043
2100
|
]
|
|
2044
2101
|
],
|
|
2045
|
-
validate: async (runtime, message, state
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
2050
|
-
const __avRegex = /\b(?:search|linear|issues)\b/i;
|
|
2051
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
2052
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
2053
|
-
const __avExpectedSource = "";
|
|
2054
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
2055
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
2056
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
2057
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
2058
|
-
return false;
|
|
2059
|
-
}
|
|
2060
|
-
const __avLegacyValidate = async (runtime2, _message, _state) => {
|
|
2061
|
-
const apiKey = runtime2.getSetting("LINEAR_API_KEY");
|
|
2062
|
-
return !!apiKey;
|
|
2063
|
-
};
|
|
2064
|
-
try {
|
|
2065
|
-
return Boolean(await __avLegacyValidate(runtime, message, state, options));
|
|
2066
|
-
} catch {
|
|
2067
|
-
return false;
|
|
2068
|
-
}
|
|
2069
|
-
},
|
|
2102
|
+
validate: async (runtime, message, state) => validateLinearActionIntent(runtime, message, state, {
|
|
2103
|
+
keywords: ["search", "linear", "issues"],
|
|
2104
|
+
regexAlternation: "search|linear|issues"
|
|
2105
|
+
}),
|
|
2070
2106
|
async handler(runtime, message, _state, _options, callback) {
|
|
2071
2107
|
try {
|
|
2072
2108
|
const linearService = runtime.getService("linear");
|
|
2073
2109
|
if (!linearService) {
|
|
2074
2110
|
throw new Error("Linear service not available");
|
|
2075
2111
|
}
|
|
2112
|
+
const accountId = getLinearAccountId(runtime, _options);
|
|
2076
2113
|
const content = message.content.text;
|
|
2077
2114
|
if (!content) {
|
|
2078
2115
|
const errorMessage = "Please provide search criteria for issues.";
|
|
@@ -2091,28 +2128,32 @@ var searchIssuesAction = {
|
|
|
2091
2128
|
filters = params.filters;
|
|
2092
2129
|
} else {
|
|
2093
2130
|
const prompt = searchTemplate.replace("{{userMessage}}", content);
|
|
2094
|
-
const response = await runtime.useModel(
|
|
2131
|
+
const response = await runtime.useModel(ModelType6.TEXT_LARGE, {
|
|
2095
2132
|
prompt
|
|
2096
2133
|
});
|
|
2097
2134
|
if (!response) {
|
|
2098
2135
|
filters = { query: content };
|
|
2099
2136
|
} else {
|
|
2100
2137
|
try {
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2138
|
+
const parsed = parseLinearPromptResponse(response);
|
|
2139
|
+
if (Object.keys(parsed).length === 0) {
|
|
2140
|
+
throw new Error("No fields found in model response");
|
|
2141
|
+
}
|
|
2103
2142
|
filters = {
|
|
2104
|
-
query: parsed.query,
|
|
2105
|
-
limit: parsed.limit || 10
|
|
2143
|
+
query: getStringValue(parsed.query),
|
|
2144
|
+
limit: getNumberValue(parsed.limit) || 10
|
|
2106
2145
|
};
|
|
2107
|
-
|
|
2108
|
-
|
|
2146
|
+
const states = getStringArrayValue(parsed.states);
|
|
2147
|
+
if (states && states.length > 0) {
|
|
2148
|
+
filters.state = states;
|
|
2109
2149
|
}
|
|
2110
|
-
|
|
2150
|
+
const assignees = getStringArrayValue(parsed.assignees);
|
|
2151
|
+
if (assignees && assignees.length > 0) {
|
|
2111
2152
|
const processedAssignees = [];
|
|
2112
|
-
for (const assignee of
|
|
2153
|
+
for (const assignee of assignees) {
|
|
2113
2154
|
if (assignee.toLowerCase() === "me") {
|
|
2114
2155
|
try {
|
|
2115
|
-
const currentUser = await linearService.getCurrentUser();
|
|
2156
|
+
const currentUser = await linearService.getCurrentUser(accountId);
|
|
2116
2157
|
processedAssignees.push(currentUser.email);
|
|
2117
2158
|
} catch {
|
|
2118
2159
|
logger9.warn('Could not resolve "me" to current user');
|
|
@@ -2125,10 +2166,11 @@ var searchIssuesAction = {
|
|
|
2125
2166
|
filters.assignee = processedAssignees;
|
|
2126
2167
|
}
|
|
2127
2168
|
}
|
|
2128
|
-
if (parsed.hasAssignee === false) {
|
|
2169
|
+
if (getBooleanValue(parsed.hasAssignee) === false) {
|
|
2129
2170
|
filters.query = filters.query ? `${filters.query} unassigned` : "unassigned";
|
|
2130
2171
|
}
|
|
2131
|
-
|
|
2172
|
+
const parsedPriorities = getStringArrayValue(parsed.priorities);
|
|
2173
|
+
if (parsedPriorities && parsedPriorities.length > 0) {
|
|
2132
2174
|
const priorityMap = {
|
|
2133
2175
|
urgent: 1,
|
|
2134
2176
|
high: 2,
|
|
@@ -2139,16 +2181,18 @@ var searchIssuesAction = {
|
|
|
2139
2181
|
"3": 3,
|
|
2140
2182
|
"4": 4
|
|
2141
2183
|
};
|
|
2142
|
-
const priorities =
|
|
2184
|
+
const priorities = parsedPriorities.map((p) => priorityMap[p.toLowerCase()]).filter(Boolean);
|
|
2143
2185
|
if (priorities.length > 0) {
|
|
2144
2186
|
filters.priority = priorities;
|
|
2145
2187
|
}
|
|
2146
2188
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2189
|
+
const teams = getStringArrayValue(parsed.teams);
|
|
2190
|
+
if (teams && teams.length > 0) {
|
|
2191
|
+
filters.team = teams[0];
|
|
2149
2192
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2193
|
+
const labels = getStringArrayValue(parsed.labels);
|
|
2194
|
+
if (labels && labels.length > 0) {
|
|
2195
|
+
filters.label = labels;
|
|
2152
2196
|
}
|
|
2153
2197
|
Object.keys(filters).forEach((key) => {
|
|
2154
2198
|
if (filters[key] === undefined) {
|
|
@@ -2162,7 +2206,7 @@ var searchIssuesAction = {
|
|
|
2162
2206
|
}
|
|
2163
2207
|
}
|
|
2164
2208
|
if (!filters.team) {
|
|
2165
|
-
const defaultTeamKey = runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
2209
|
+
const defaultTeamKey = linearService.getDefaultTeamKey(accountId) ?? runtime.getSetting("LINEAR_DEFAULT_TEAM_KEY");
|
|
2166
2210
|
if (defaultTeamKey) {
|
|
2167
2211
|
const searchingAllIssues = content.toLowerCase().includes("all") && (content.toLowerCase().includes("issue") || content.toLowerCase().includes("bug") || content.toLowerCase().includes("task"));
|
|
2168
2212
|
if (!searchingAllIssues) {
|
|
@@ -2172,7 +2216,7 @@ var searchIssuesAction = {
|
|
|
2172
2216
|
}
|
|
2173
2217
|
}
|
|
2174
2218
|
filters.limit = params?.limit ?? filters.limit ?? 10;
|
|
2175
|
-
const issues = await linearService.searchIssues(filters);
|
|
2219
|
+
const issues = await linearService.searchIssues(filters, accountId);
|
|
2176
2220
|
if (issues.length === 0) {
|
|
2177
2221
|
const noResultsMessage = "No issues found matching your search criteria.";
|
|
2178
2222
|
await callback?.({
|
|
@@ -2185,7 +2229,8 @@ var searchIssuesAction = {
|
|
|
2185
2229
|
data: {
|
|
2186
2230
|
issues: [],
|
|
2187
2231
|
filters: filters ? { ...filters } : undefined,
|
|
2188
|
-
count: 0
|
|
2232
|
+
count: 0,
|
|
2233
|
+
accountId
|
|
2189
2234
|
}
|
|
2190
2235
|
};
|
|
2191
2236
|
}
|
|
@@ -2229,7 +2274,8 @@ ${issueText}`;
|
|
|
2229
2274
|
};
|
|
2230
2275
|
})),
|
|
2231
2276
|
filters: filters ? { ...filters } : undefined,
|
|
2232
|
-
count: issues.length
|
|
2277
|
+
count: issues.length,
|
|
2278
|
+
accountId
|
|
2233
2279
|
}
|
|
2234
2280
|
};
|
|
2235
2281
|
} catch (error) {
|
|
@@ -2247,223 +2293,159 @@ ${issueText}`;
|
|
|
2247
2293
|
}
|
|
2248
2294
|
};
|
|
2249
2295
|
|
|
2250
|
-
// src/actions/
|
|
2296
|
+
// src/actions/updateComment.ts
|
|
2251
2297
|
import {
|
|
2252
|
-
logger as logger10
|
|
2253
|
-
ModelType as ModelType9
|
|
2298
|
+
logger as logger10
|
|
2254
2299
|
} from "@elizaos/core";
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
"
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
{
|
|
2268
|
-
|
|
2269
|
-
content: {
|
|
2270
|
-
text: 'Update issue ENG-123 title to "Fix login button on all devices"'
|
|
2271
|
-
}
|
|
2272
|
-
},
|
|
2273
|
-
{
|
|
2274
|
-
name: "Assistant",
|
|
2275
|
-
content: {
|
|
2276
|
-
text: "I'll update the title of issue ENG-123 for you.",
|
|
2277
|
-
actions: ["UPDATE_LINEAR_ISSUE"]
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
],
|
|
2281
|
-
[
|
|
2282
|
-
{
|
|
2283
|
-
name: "User",
|
|
2284
|
-
content: {
|
|
2285
|
-
text: "Move issue COM2-7 to the ELIZA team"
|
|
2286
|
-
}
|
|
2287
|
-
},
|
|
2288
|
-
{
|
|
2289
|
-
name: "Assistant",
|
|
2290
|
-
content: {
|
|
2291
|
-
text: "I'll move issue COM2-7 to the ELIZA team.",
|
|
2292
|
-
actions: ["UPDATE_LINEAR_ISSUE"]
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2295
|
-
],
|
|
2296
|
-
[
|
|
2297
|
-
{
|
|
2298
|
-
name: "User",
|
|
2299
|
-
content: {
|
|
2300
|
-
text: "Change the priority of BUG-456 to high and assign to john@example.com"
|
|
2301
|
-
}
|
|
2302
|
-
},
|
|
2303
|
-
{
|
|
2304
|
-
name: "Assistant",
|
|
2305
|
-
content: {
|
|
2306
|
-
text: "I'll change the priority of BUG-456 to high and assign it to john@example.com.",
|
|
2307
|
-
actions: ["UPDATE_LINEAR_ISSUE"]
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
]
|
|
2311
|
-
],
|
|
2312
|
-
validate: async (runtime, message, state, options) => {
|
|
2313
|
-
const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
|
|
2314
|
-
const __avText = __avTextRaw.toLowerCase();
|
|
2315
|
-
const __avKeywords = ["update", "linear", "issue"];
|
|
2316
|
-
const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw));
|
|
2317
|
-
const __avRegex = /\b(?:update|linear|issue)\b/i;
|
|
2318
|
-
const __avRegexOk = __avRegex.test(__avText);
|
|
2319
|
-
const __avSource = String(message?.content?.source ?? message?.source ?? "");
|
|
2320
|
-
const __avExpectedSource = "";
|
|
2321
|
-
const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
|
|
2322
|
-
const __avOptions = options && typeof options === "object" ? options : {};
|
|
2323
|
-
const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object");
|
|
2324
|
-
if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
|
|
2325
|
-
return false;
|
|
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 };
|
|
2326
2314
|
}
|
|
2327
|
-
const
|
|
2328
|
-
|
|
2329
|
-
|
|
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 }
|
|
2330
2322
|
};
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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");
|
|
2335
2343
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
const
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
const content = message.content.text;
|
|
2344
|
-
if (!content) {
|
|
2345
|
-
const errorMessage = "Please provide update instructions for the issue.";
|
|
2346
|
-
await callback?.({
|
|
2347
|
-
text: errorMessage,
|
|
2348
|
-
source: message.content.source
|
|
2349
|
-
});
|
|
2350
|
-
return {
|
|
2351
|
-
text: errorMessage,
|
|
2352
|
-
success: false
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
|
-
const prompt = updateIssueTemplate.replace("{{userMessage}}", content);
|
|
2356
|
-
const response = await runtime.useModel(ModelType9.TEXT_LARGE, {
|
|
2357
|
-
prompt
|
|
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
|
|
2358
2351
|
});
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
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`);
|
|
2389
2400
|
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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`);
|
|
2399
2411
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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}`);
|
|
2406
2426
|
} else {
|
|
2407
|
-
|
|
2408
|
-
const state = states.find((s) => s.name.toLowerCase() === parsed.updates.status.toLowerCase() || s.type.toLowerCase() === parsed.updates.status.toLowerCase());
|
|
2409
|
-
if (state) {
|
|
2410
|
-
updates.stateId = state.id;
|
|
2411
|
-
logger10.info(`Changing status to: ${state.name}`);
|
|
2412
|
-
} else {
|
|
2413
|
-
logger10.warn(`Status ${parsed.updates.status} not found for team`);
|
|
2414
|
-
}
|
|
2427
|
+
logger11.warn(`Status ${status} not found for team`);
|
|
2415
2428
|
}
|
|
2416
2429
|
}
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
}
|
|
2428
|
-
}
|
|
2429
|
-
updates.labelIds = labelIds;
|
|
2430
|
-
}
|
|
2431
|
-
} catch (parseError) {
|
|
2432
|
-
logger10.warn("Failed to parse LLM response, falling back to regex parsing:", parseError);
|
|
2433
|
-
const issueMatch = content.match(/(\w+-\d+)/);
|
|
2434
|
-
if (!issueMatch) {
|
|
2435
|
-
const errorMessage = "Please specify an issue ID (e.g., ENG-123) to update.";
|
|
2436
|
-
await callback?.({
|
|
2437
|
-
text: errorMessage,
|
|
2438
|
-
source: message.content.source
|
|
2439
|
-
});
|
|
2440
|
-
return {
|
|
2441
|
-
text: errorMessage,
|
|
2442
|
-
success: false
|
|
2443
|
-
};
|
|
2444
|
-
}
|
|
2445
|
-
issueId = issueMatch[1];
|
|
2446
|
-
const titleMatch = content.match(/title to ["'](.+?)["']/i);
|
|
2447
|
-
if (titleMatch) {
|
|
2448
|
-
updates.title = titleMatch[1];
|
|
2449
|
-
}
|
|
2450
|
-
const priorityMatch = content.match(/priority (?:to |as )?(\w+)/i);
|
|
2451
|
-
if (priorityMatch) {
|
|
2452
|
-
const priorityMap = {
|
|
2453
|
-
urgent: 1,
|
|
2454
|
-
high: 2,
|
|
2455
|
-
normal: 3,
|
|
2456
|
-
medium: 3,
|
|
2457
|
-
low: 4
|
|
2458
|
-
};
|
|
2459
|
-
const priority = priorityMap[priorityMatch[1].toLowerCase()];
|
|
2460
|
-
if (priority) {
|
|
2461
|
-
updates.priority = priority;
|
|
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);
|
|
2462
2440
|
}
|
|
2463
2441
|
}
|
|
2442
|
+
updates.labelIds = labelIds;
|
|
2464
2443
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
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.";
|
|
2467
2449
|
await callback?.({
|
|
2468
2450
|
text: errorMessage,
|
|
2469
2451
|
source: message.content.source
|
|
@@ -2473,43 +2455,28 @@ var updateIssueAction = {
|
|
|
2473
2455
|
success: false
|
|
2474
2456
|
};
|
|
2475
2457
|
}
|
|
2476
|
-
|
|
2477
|
-
const
|
|
2478
|
-
if (
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
if (
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
await callback?.({
|
|
2494
|
-
text: successMessage,
|
|
2495
|
-
source: message.content.source
|
|
2496
|
-
});
|
|
2497
|
-
return {
|
|
2498
|
-
text: `Updated issue ${updatedIssue.identifier}: ${updateSummary.join(", ")}`,
|
|
2499
|
-
success: true,
|
|
2500
|
-
data: {
|
|
2501
|
-
issueId: updatedIssue.id,
|
|
2502
|
-
identifier: updatedIssue.identifier,
|
|
2503
|
-
updates: updates ? Object.fromEntries(Object.entries(updates).map(([key, value]) => [
|
|
2504
|
-
key,
|
|
2505
|
-
value instanceof Date ? value.toISOString() : value
|
|
2506
|
-
])) : undefined,
|
|
2507
|
-
url: updatedIssue.url
|
|
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;
|
|
2508
2475
|
}
|
|
2509
|
-
}
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
const errorMessage =
|
|
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'")`;
|
|
2513
2480
|
await callback?.({
|
|
2514
2481
|
text: errorMessage,
|
|
2515
2482
|
source: message.content.source
|
|
@@ -2519,14 +2486,484 @@ View it at: ${updatedIssue.url}`;
|
|
|
2519
2486
|
success: false
|
|
2520
2487
|
};
|
|
2521
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
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
|
|
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
|
|
2522
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.",
|
|
2642
|
+
similes: [
|
|
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"
|
|
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
|
+
},
|
|
2708
|
+
examples: [
|
|
2709
|
+
[
|
|
2710
|
+
{ name: "{{user1}}", content: { text: "Create a Linear issue for the mobile login bug" } },
|
|
2711
|
+
{
|
|
2712
|
+
name: "{{agentName}}",
|
|
2713
|
+
content: {
|
|
2714
|
+
text: "I'll create that Linear issue.",
|
|
2715
|
+
actions: ["LINEAR"]
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
],
|
|
2719
|
+
[
|
|
2720
|
+
{ name: "{{user1}}", content: { text: "Comment on ENG-123 that QA can retest it" } },
|
|
2721
|
+
{
|
|
2722
|
+
name: "{{agentName}}",
|
|
2723
|
+
content: { text: "I'll add that comment to ENG-123.", actions: ["LINEAR"] }
|
|
2724
|
+
}
|
|
2725
|
+
],
|
|
2726
|
+
[
|
|
2727
|
+
{ name: "{{user1}}", content: { text: "Search open Linear bugs for the backend team" } },
|
|
2728
|
+
{
|
|
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?" } },
|
|
2735
|
+
{
|
|
2736
|
+
name: "{{agentName}}",
|
|
2737
|
+
content: { text: "Looking up ENG-456.", actions: ["LINEAR"] }
|
|
2738
|
+
}
|
|
2739
|
+
]
|
|
2740
|
+
]
|
|
2523
2741
|
};
|
|
2524
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
|
|
2837
|
+
}
|
|
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"
|
|
2858
|
+
};
|
|
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"
|
|
2875
|
+
});
|
|
2876
|
+
return {
|
|
2877
|
+
authUrl: `${LINEAR_AUTHORIZATION_ENDPOINT}?${params.toString()}`,
|
|
2878
|
+
metadata: {
|
|
2879
|
+
...request.metadata,
|
|
2880
|
+
requestedScopes: scopes,
|
|
2881
|
+
redirectUri
|
|
2882
|
+
}
|
|
2883
|
+
};
|
|
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
|
|
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
|
+
return {
|
|
2938
|
+
account: accountPatch,
|
|
2939
|
+
flow: { status: "completed" }
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
};
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2525
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
|
+
}
|
|
2526
2958
|
var linearActivityProvider = {
|
|
2527
2959
|
name: "LINEAR_ACTIVITY",
|
|
2528
2960
|
description: "Provides context about recent Linear activity",
|
|
2961
|
+
descriptionCompressed: "provide context recent Linear activity",
|
|
2529
2962
|
dynamic: true,
|
|
2963
|
+
contexts: ["automation", "connectors"],
|
|
2964
|
+
contextGate: { anyOf: ["automation", "connectors"] },
|
|
2965
|
+
cacheScope: "turn",
|
|
2966
|
+
roleGate: { minRole: "ADMIN" },
|
|
2530
2967
|
get: async (runtime, _message, _state) => {
|
|
2531
2968
|
try {
|
|
2532
2969
|
const linearService = runtime.getService("linear");
|
|
@@ -2559,7 +2996,7 @@ ${activityList.join(`
|
|
|
2559
2996
|
resource_id: item.resource_id,
|
|
2560
2997
|
success: item.success,
|
|
2561
2998
|
error: item.error,
|
|
2562
|
-
details:
|
|
2999
|
+
details: formatDetails(item.details),
|
|
2563
3000
|
timestamp: typeof item.timestamp === "string" ? item.timestamp : new Date(item.timestamp).toISOString()
|
|
2564
3001
|
}))
|
|
2565
3002
|
}
|
|
@@ -2576,7 +3013,12 @@ ${activityList.join(`
|
|
|
2576
3013
|
var linearIssuesProvider = {
|
|
2577
3014
|
name: "LINEAR_ISSUES",
|
|
2578
3015
|
description: "Provides context about recent Linear issues",
|
|
3016
|
+
descriptionCompressed: "provide context recent Linear issue",
|
|
2579
3017
|
dynamic: true,
|
|
3018
|
+
contexts: ["automation", "connectors"],
|
|
3019
|
+
contextGate: { anyOf: ["automation", "connectors"] },
|
|
3020
|
+
cacheScope: "turn",
|
|
3021
|
+
roleGate: { minRole: "ADMIN" },
|
|
2580
3022
|
get: async (runtime, _message, _state) => {
|
|
2581
3023
|
try {
|
|
2582
3024
|
const linearService = runtime.getService("linear");
|
|
@@ -2620,7 +3062,12 @@ ${issuesList.join(`
|
|
|
2620
3062
|
var linearProjectsProvider = {
|
|
2621
3063
|
name: "LINEAR_PROJECTS",
|
|
2622
3064
|
description: "Provides context about active Linear projects",
|
|
3065
|
+
descriptionCompressed: "provide context active Linear project",
|
|
2623
3066
|
dynamic: true,
|
|
3067
|
+
contexts: ["automation", "connectors"],
|
|
3068
|
+
contextGate: { anyOf: ["automation", "connectors"] },
|
|
3069
|
+
cacheScope: "agent",
|
|
3070
|
+
roleGate: { minRole: "ADMIN" },
|
|
2624
3071
|
get: async (runtime, _message, _state) => {
|
|
2625
3072
|
try {
|
|
2626
3073
|
const linearService = runtime.getService("linear");
|
|
@@ -2659,10 +3106,17 @@ ${projectsList.join(`
|
|
|
2659
3106
|
};
|
|
2660
3107
|
|
|
2661
3108
|
// src/providers/teams.ts
|
|
3109
|
+
var MAX_LINEAR_TEAMS = 20;
|
|
3110
|
+
var MAX_DESCRIPTION_CHARS = 180;
|
|
2662
3111
|
var linearTeamsProvider = {
|
|
2663
3112
|
name: "LINEAR_TEAMS",
|
|
2664
3113
|
description: "Provides context about Linear teams",
|
|
3114
|
+
descriptionCompressed: "provide context Linear team",
|
|
2665
3115
|
dynamic: true,
|
|
3116
|
+
contexts: ["automation", "connectors"],
|
|
3117
|
+
contextGate: { anyOf: ["automation", "connectors"] },
|
|
3118
|
+
cacheScope: "agent",
|
|
3119
|
+
roleGate: { minRole: "ADMIN" },
|
|
2666
3120
|
get: async (runtime, _message, _state) => {
|
|
2667
3121
|
try {
|
|
2668
3122
|
const linearService = runtime.getService("linear");
|
|
@@ -2672,23 +3126,25 @@ var linearTeamsProvider = {
|
|
|
2672
3126
|
};
|
|
2673
3127
|
}
|
|
2674
3128
|
const teams = await linearService.getTeams();
|
|
3129
|
+
const listedTeams = teams.slice(0, MAX_LINEAR_TEAMS);
|
|
2675
3130
|
if (teams.length === 0) {
|
|
2676
3131
|
return {
|
|
2677
3132
|
text: "No Linear teams found"
|
|
2678
3133
|
};
|
|
2679
3134
|
}
|
|
2680
|
-
const teamsList =
|
|
3135
|
+
const teamsList = listedTeams.map((team) => `- ${team.name} (${team.key}): ${(team.description || "No description").slice(0, MAX_DESCRIPTION_CHARS)}`);
|
|
2681
3136
|
const text = `Linear Teams:
|
|
2682
3137
|
${teamsList.join(`
|
|
2683
3138
|
`)}`;
|
|
2684
3139
|
return {
|
|
2685
3140
|
text,
|
|
2686
3141
|
data: {
|
|
2687
|
-
teams:
|
|
3142
|
+
teams: listedTeams.map((team) => ({
|
|
2688
3143
|
id: team.id,
|
|
2689
3144
|
name: team.name,
|
|
2690
3145
|
key: team.key
|
|
2691
|
-
}))
|
|
3146
|
+
})),
|
|
3147
|
+
truncated: teams.length > listedTeams.length
|
|
2692
3148
|
}
|
|
2693
3149
|
};
|
|
2694
3150
|
} catch (_error) {
|
|
@@ -2699,8 +3155,85 @@ ${teamsList.join(`
|
|
|
2699
3155
|
}
|
|
2700
3156
|
};
|
|
2701
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
|
+
|
|
2702
3235
|
// src/services/linear.ts
|
|
2703
|
-
import { logger as
|
|
3236
|
+
import { logger as logger13, Service } from "@elizaos/core";
|
|
2704
3237
|
import {
|
|
2705
3238
|
LinearClient
|
|
2706
3239
|
} from "@linear/sdk";
|
|
@@ -2728,44 +3261,64 @@ class LinearAuthenticationError extends LinearAPIError {
|
|
|
2728
3261
|
class LinearService extends Service {
|
|
2729
3262
|
static serviceType = "linear";
|
|
2730
3263
|
capabilityDescription = "Linear API integration for issue tracking, project management, and team collaboration";
|
|
2731
|
-
|
|
3264
|
+
clients = new Map;
|
|
2732
3265
|
activityLog = [];
|
|
2733
|
-
|
|
3266
|
+
defaultAccountId = DEFAULT_LINEAR_ACCOUNT_ID;
|
|
2734
3267
|
workspaceId;
|
|
2735
3268
|
constructor(runtime) {
|
|
2736
3269
|
super(runtime);
|
|
2737
|
-
const
|
|
2738
|
-
const
|
|
2739
|
-
|
|
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) {
|
|
2740
3274
|
throw new LinearAuthenticationError("Linear API key is required");
|
|
2741
3275
|
}
|
|
2742
|
-
this.
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
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
|
+
}
|
|
2750
3285
|
}
|
|
2751
3286
|
static async start(runtime) {
|
|
2752
3287
|
const service = new LinearService(runtime);
|
|
2753
3288
|
await service.validateConnection();
|
|
2754
|
-
|
|
3289
|
+
logger13.info("Linear service started successfully");
|
|
2755
3290
|
return service;
|
|
2756
3291
|
}
|
|
2757
3292
|
async stop() {
|
|
2758
3293
|
this.activityLog = [];
|
|
2759
|
-
|
|
3294
|
+
logger13.info("Linear service stopped");
|
|
2760
3295
|
}
|
|
2761
|
-
async validateConnection() {
|
|
3296
|
+
async validateConnection(accountId) {
|
|
2762
3297
|
try {
|
|
2763
|
-
const
|
|
2764
|
-
|
|
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})`);
|
|
2765
3301
|
} catch (_error) {
|
|
2766
3302
|
throw new LinearAuthenticationError("Failed to authenticate with Linear API");
|
|
2767
3303
|
}
|
|
2768
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
|
+
}
|
|
2769
3322
|
logActivity(action, resourceType, resourceId, details, success, error) {
|
|
2770
3323
|
const activity = {
|
|
2771
3324
|
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
@@ -2782,7 +3335,7 @@ class LinearService extends Service {
|
|
|
2782
3335
|
this.activityLog = this.activityLog.slice(-1000);
|
|
2783
3336
|
}
|
|
2784
3337
|
}
|
|
2785
|
-
getActivityLog(limit, filter) {
|
|
3338
|
+
getActivityLog(limit, filter, accountId) {
|
|
2786
3339
|
let filtered = [...this.activityLog];
|
|
2787
3340
|
if (filter) {
|
|
2788
3341
|
filtered = filtered.filter((item) => {
|
|
@@ -2791,25 +3344,36 @@ class LinearService extends Service {
|
|
|
2791
3344
|
});
|
|
2792
3345
|
});
|
|
2793
3346
|
}
|
|
3347
|
+
if (accountId) {
|
|
3348
|
+
filtered = filtered.filter((item) => item.details.accountId === accountId);
|
|
3349
|
+
}
|
|
2794
3350
|
return filtered.slice(-(limit || 100));
|
|
2795
3351
|
}
|
|
2796
|
-
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
|
+
}
|
|
2797
3358
|
this.activityLog = [];
|
|
2798
|
-
|
|
3359
|
+
logger13.info("Linear activity log cleared");
|
|
2799
3360
|
}
|
|
2800
|
-
async getTeams() {
|
|
2801
|
-
const
|
|
3361
|
+
async getTeams(accountId) {
|
|
3362
|
+
const state = this.getAccountState(accountId);
|
|
3363
|
+
const teams = await state.client.teams();
|
|
2802
3364
|
const teamList = await teams.nodes;
|
|
2803
|
-
this.logActivity("list_teams", "team", "all", { count: teamList.length }, true);
|
|
3365
|
+
this.logActivity("list_teams", "team", "all", { count: teamList.length, accountId: state.accountId }, true);
|
|
2804
3366
|
return teamList;
|
|
2805
3367
|
}
|
|
2806
|
-
async getTeam(teamId) {
|
|
2807
|
-
const
|
|
2808
|
-
|
|
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);
|
|
2809
3372
|
return team;
|
|
2810
3373
|
}
|
|
2811
|
-
async createIssue(input) {
|
|
2812
|
-
const
|
|
3374
|
+
async createIssue(input, accountId) {
|
|
3375
|
+
const state = this.getAccountState(accountId);
|
|
3376
|
+
const issuePayload = await state.client.createIssue({
|
|
2813
3377
|
title: input.title,
|
|
2814
3378
|
description: input.description,
|
|
2815
3379
|
teamId: input.teamId,
|
|
@@ -2827,20 +3391,24 @@ class LinearService extends Service {
|
|
|
2827
3391
|
}
|
|
2828
3392
|
this.logActivity("create_issue", "issue", issue.id, {
|
|
2829
3393
|
title: input.title,
|
|
2830
|
-
teamId: input.teamId
|
|
3394
|
+
teamId: input.teamId,
|
|
3395
|
+
accountId: state.accountId
|
|
2831
3396
|
}, true);
|
|
2832
3397
|
return issue;
|
|
2833
3398
|
}
|
|
2834
|
-
async getIssue(issueId) {
|
|
2835
|
-
const
|
|
3399
|
+
async getIssue(issueId, accountId) {
|
|
3400
|
+
const state = this.getAccountState(accountId);
|
|
3401
|
+
const issue = await state.client.issue(issueId);
|
|
2836
3402
|
this.logActivity("get_issue", "issue", issueId, {
|
|
2837
3403
|
title: issue.title,
|
|
2838
|
-
identifier: issue.identifier
|
|
3404
|
+
identifier: issue.identifier,
|
|
3405
|
+
accountId: state.accountId
|
|
2839
3406
|
}, true);
|
|
2840
3407
|
return issue;
|
|
2841
3408
|
}
|
|
2842
|
-
async updateIssue(issueId, updates) {
|
|
2843
|
-
const
|
|
3409
|
+
async updateIssue(issueId, updates, accountId) {
|
|
3410
|
+
const state = this.getAccountState(accountId);
|
|
3411
|
+
const updatePayload = await state.client.updateIssue(issueId, {
|
|
2844
3412
|
title: updates.title,
|
|
2845
3413
|
description: updates.description,
|
|
2846
3414
|
priority: updates.priority,
|
|
@@ -2855,18 +3423,20 @@ class LinearService extends Service {
|
|
|
2855
3423
|
if (!issue) {
|
|
2856
3424
|
throw new Error("Failed to update issue");
|
|
2857
3425
|
}
|
|
2858
|
-
this.logActivity("update_issue", "issue", issueId, updates, true);
|
|
3426
|
+
this.logActivity("update_issue", "issue", issueId, { ...updates, accountId: state.accountId }, true);
|
|
2859
3427
|
return issue;
|
|
2860
3428
|
}
|
|
2861
|
-
async deleteIssue(issueId) {
|
|
2862
|
-
const
|
|
3429
|
+
async deleteIssue(issueId, accountId) {
|
|
3430
|
+
const state = this.getAccountState(accountId);
|
|
3431
|
+
const archivePayload = await state.client.archiveIssue(issueId);
|
|
2863
3432
|
const success = await archivePayload.success;
|
|
2864
3433
|
if (!success) {
|
|
2865
3434
|
throw new Error("Failed to archive issue");
|
|
2866
3435
|
}
|
|
2867
|
-
this.logActivity("delete_issue", "issue", issueId, { action: "archived" }, true);
|
|
3436
|
+
this.logActivity("delete_issue", "issue", issueId, { action: "archived", accountId: state.accountId }, true);
|
|
2868
3437
|
}
|
|
2869
|
-
async searchIssues(filters) {
|
|
3438
|
+
async searchIssues(filters, accountId) {
|
|
3439
|
+
const state = this.getAccountState(accountId);
|
|
2870
3440
|
const filterObject = {};
|
|
2871
3441
|
if (filters.query) {
|
|
2872
3442
|
filterObject.or = [
|
|
@@ -2875,14 +3445,14 @@ class LinearService extends Service {
|
|
|
2875
3445
|
];
|
|
2876
3446
|
}
|
|
2877
3447
|
if (filters.team) {
|
|
2878
|
-
const teams = await this.getTeams();
|
|
3448
|
+
const teams = await this.getTeams(state.accountId);
|
|
2879
3449
|
const team = teams.find((t) => t.key.toLowerCase() === filters.team?.toLowerCase() || t.name.toLowerCase() === filters.team?.toLowerCase());
|
|
2880
3450
|
if (team) {
|
|
2881
3451
|
filterObject.team = { id: { eq: team.id } };
|
|
2882
3452
|
}
|
|
2883
3453
|
}
|
|
2884
3454
|
if (filters.assignee && filters.assignee.length > 0) {
|
|
2885
|
-
const users = await this.getUsers();
|
|
3455
|
+
const users = await this.getUsers(state.accountId);
|
|
2886
3456
|
const assigneeIds = filters.assignee.map((assigneeName) => {
|
|
2887
3457
|
const user = users.find((u) => u.email === assigneeName || u.name.toLowerCase().includes(assigneeName.toLowerCase()));
|
|
2888
3458
|
return user?.id;
|
|
@@ -2906,20 +3476,21 @@ class LinearService extends Service {
|
|
|
2906
3476
|
}
|
|
2907
3477
|
};
|
|
2908
3478
|
}
|
|
2909
|
-
const query =
|
|
3479
|
+
const query = state.client.issues({
|
|
2910
3480
|
first: filters.limit || 50,
|
|
2911
3481
|
filter: Object.keys(filterObject).length > 0 ? filterObject : undefined
|
|
2912
3482
|
});
|
|
2913
3483
|
const issues = await query;
|
|
2914
3484
|
const issueList = await issues.nodes;
|
|
2915
3485
|
this.logActivity("search_issues", "issue", "search", {
|
|
2916
|
-
filters: { ...filters },
|
|
3486
|
+
filters: { ...filters, accountId: state.accountId },
|
|
2917
3487
|
count: issueList.length
|
|
2918
3488
|
}, true);
|
|
2919
3489
|
return issueList;
|
|
2920
3490
|
}
|
|
2921
|
-
async createComment(input) {
|
|
2922
|
-
const
|
|
3491
|
+
async createComment(input, accountId) {
|
|
3492
|
+
const state = this.getAccountState(accountId);
|
|
3493
|
+
const commentPayload = await state.client.createComment({
|
|
2923
3494
|
body: input.body,
|
|
2924
3495
|
issueId: input.issueId
|
|
2925
3496
|
});
|
|
@@ -2929,12 +3500,39 @@ class LinearService extends Service {
|
|
|
2929
3500
|
}
|
|
2930
3501
|
this.logActivity("create_comment", "comment", comment.id, {
|
|
2931
3502
|
issueId: input.issueId,
|
|
2932
|
-
bodyLength: input.body.length
|
|
3503
|
+
bodyLength: input.body.length,
|
|
3504
|
+
accountId: state.accountId
|
|
2933
3505
|
}, true);
|
|
2934
3506
|
return comment;
|
|
2935
3507
|
}
|
|
2936
|
-
async
|
|
2937
|
-
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({
|
|
2938
3536
|
first: 100
|
|
2939
3537
|
});
|
|
2940
3538
|
const projects = await query;
|
|
@@ -2950,44 +3548,54 @@ class LinearService extends Service {
|
|
|
2950
3548
|
}
|
|
2951
3549
|
this.logActivity("list_projects", "project", "all", {
|
|
2952
3550
|
count: projectList.length,
|
|
2953
|
-
teamId
|
|
3551
|
+
teamId,
|
|
3552
|
+
accountId: state.accountId
|
|
2954
3553
|
}, true);
|
|
2955
3554
|
return projectList;
|
|
2956
3555
|
}
|
|
2957
|
-
async getProject(projectId) {
|
|
2958
|
-
const
|
|
3556
|
+
async getProject(projectId, accountId) {
|
|
3557
|
+
const state = this.getAccountState(accountId);
|
|
3558
|
+
const project = await state.client.project(projectId);
|
|
2959
3559
|
this.logActivity("get_project", "project", projectId, {
|
|
2960
|
-
name: project.name
|
|
3560
|
+
name: project.name,
|
|
3561
|
+
accountId: state.accountId
|
|
2961
3562
|
}, true);
|
|
2962
3563
|
return project;
|
|
2963
3564
|
}
|
|
2964
|
-
async getUsers() {
|
|
2965
|
-
const
|
|
3565
|
+
async getUsers(accountId) {
|
|
3566
|
+
const state = this.getAccountState(accountId);
|
|
3567
|
+
const users = await state.client.users();
|
|
2966
3568
|
const userList = await users.nodes;
|
|
2967
3569
|
this.logActivity("list_users", "user", "all", {
|
|
2968
|
-
count: userList.length
|
|
3570
|
+
count: userList.length,
|
|
3571
|
+
accountId: state.accountId
|
|
2969
3572
|
}, true);
|
|
2970
3573
|
return userList;
|
|
2971
3574
|
}
|
|
2972
|
-
async getCurrentUser() {
|
|
2973
|
-
const
|
|
3575
|
+
async getCurrentUser(accountId) {
|
|
3576
|
+
const state = this.getAccountState(accountId);
|
|
3577
|
+
const user = await state.client.viewer;
|
|
2974
3578
|
this.logActivity("get_current_user", "user", user.id, {
|
|
2975
3579
|
email: user.email,
|
|
2976
|
-
name: user.name
|
|
3580
|
+
name: user.name,
|
|
3581
|
+
accountId: state.accountId
|
|
2977
3582
|
}, true);
|
|
2978
3583
|
return user;
|
|
2979
3584
|
}
|
|
2980
|
-
async getUserTeams() {
|
|
2981
|
-
const
|
|
3585
|
+
async getUserTeams(accountId) {
|
|
3586
|
+
const state = this.getAccountState(accountId);
|
|
3587
|
+
const viewer = await state.client.viewer;
|
|
2982
3588
|
const teams = await viewer.teams();
|
|
2983
3589
|
const teamList = await teams.nodes;
|
|
2984
3590
|
this.logActivity("list_user_teams", "team", viewer.id, {
|
|
2985
|
-
count: teamList.length
|
|
3591
|
+
count: teamList.length,
|
|
3592
|
+
accountId: state.accountId
|
|
2986
3593
|
}, true);
|
|
2987
3594
|
return teamList;
|
|
2988
3595
|
}
|
|
2989
|
-
async getLabels(teamId) {
|
|
2990
|
-
const
|
|
3596
|
+
async getLabels(teamId, accountId) {
|
|
3597
|
+
const state = this.getAccountState(accountId);
|
|
3598
|
+
const query = state.client.issueLabels({
|
|
2991
3599
|
first: 100,
|
|
2992
3600
|
filter: teamId ? {
|
|
2993
3601
|
team: { id: { eq: teamId } }
|
|
@@ -2997,19 +3605,22 @@ class LinearService extends Service {
|
|
|
2997
3605
|
const labelList = await labels.nodes;
|
|
2998
3606
|
this.logActivity("list_labels", "label", "all", {
|
|
2999
3607
|
count: labelList.length,
|
|
3000
|
-
teamId
|
|
3608
|
+
teamId,
|
|
3609
|
+
accountId: state.accountId
|
|
3001
3610
|
}, true);
|
|
3002
3611
|
return labelList;
|
|
3003
3612
|
}
|
|
3004
|
-
async getWorkflowStates(teamId) {
|
|
3005
|
-
const
|
|
3613
|
+
async getWorkflowStates(teamId, accountId) {
|
|
3614
|
+
const state = this.getAccountState(accountId);
|
|
3615
|
+
const states = await state.client.workflowStates({
|
|
3006
3616
|
filter: {
|
|
3007
3617
|
team: { id: { eq: teamId } }
|
|
3008
3618
|
}
|
|
3009
3619
|
});
|
|
3010
3620
|
const stateList = await states.nodes;
|
|
3011
3621
|
this.logActivity("list_workflow_states", "team", teamId, {
|
|
3012
|
-
count: stateList.length
|
|
3622
|
+
count: stateList.length,
|
|
3623
|
+
accountId: state.accountId
|
|
3013
3624
|
}, true);
|
|
3014
3625
|
return stateList;
|
|
3015
3626
|
}
|
|
@@ -3020,28 +3631,38 @@ var linearPlugin = {
|
|
|
3020
3631
|
name: "@elizaos/plugin-linear-ts",
|
|
3021
3632
|
description: "Plugin for integrating with Linear issue tracking system",
|
|
3022
3633
|
services: [LinearService],
|
|
3023
|
-
actions: [
|
|
3024
|
-
createIssueAction,
|
|
3025
|
-
getIssueAction,
|
|
3026
|
-
updateIssueAction,
|
|
3027
|
-
deleteIssueAction,
|
|
3028
|
-
searchIssuesAction,
|
|
3029
|
-
createCommentAction,
|
|
3030
|
-
listTeamsAction,
|
|
3031
|
-
listProjectsAction,
|
|
3032
|
-
getActivityAction,
|
|
3033
|
-
clearActivityAction
|
|
3034
|
-
],
|
|
3634
|
+
actions: [...promoteSubactionsToActions(linearAction)],
|
|
3035
3635
|
providers: [
|
|
3036
3636
|
linearIssuesProvider,
|
|
3037
3637
|
linearTeamsProvider,
|
|
3038
3638
|
linearProjectsProvider,
|
|
3039
3639
|
linearActivityProvider
|
|
3040
|
-
]
|
|
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
|
+
}
|
|
3041
3653
|
};
|
|
3042
3654
|
export {
|
|
3655
|
+
resolveLinearDefaultAccount,
|
|
3656
|
+
resolveLinearAccountId,
|
|
3657
|
+
resolveLinearAccount,
|
|
3658
|
+
readLinearAccounts,
|
|
3659
|
+
normalizeLinearAccountId,
|
|
3043
3660
|
linearPlugin,
|
|
3044
|
-
|
|
3661
|
+
hasLinearAccountConfig,
|
|
3662
|
+
createLinearConnectorAccountProvider,
|
|
3663
|
+
LinearService,
|
|
3664
|
+
DEFAULT_LINEAR_ACCOUNT_ROLE,
|
|
3665
|
+
DEFAULT_LINEAR_ACCOUNT_ID
|
|
3045
3666
|
};
|
|
3046
3667
|
|
|
3047
|
-
//# debugId=
|
|
3668
|
+
//# debugId=F5689DC5E2BC1C6664756E2164756E21
|