@elizaos/plugin-github 2.0.3-beta.6 → 2.0.3-beta.7
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.d.ts +295 -0
- package/dist/index.js +2405 -0
- package/dist/register-routes.d.ts +2 -0
- package/dist/register-routes.js +6 -0
- package/package.json +4 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,2405 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
getConnectorAccountManager as getConnectorAccountManager2,
|
|
4
|
+
logger as logger7,
|
|
5
|
+
promoteSubactionsToActions
|
|
6
|
+
} from "@elizaos/core";
|
|
7
|
+
|
|
8
|
+
// src/actions/issue-op.ts
|
|
9
|
+
import { logger, requireConfirmation } from "@elizaos/core";
|
|
10
|
+
|
|
11
|
+
// src/connector-credential-refs.ts
|
|
12
|
+
import {
|
|
13
|
+
CONNECTOR_ACCOUNT_STORAGE_SERVICE_TYPE,
|
|
14
|
+
getConnectorAccountManager
|
|
15
|
+
} from "@elizaos/core";
|
|
16
|
+
var OAUTH_TOKENS_CREDENTIAL_TYPE = "oauth.tokens";
|
|
17
|
+
async function persistConnectorCredentialRefs(params) {
|
|
18
|
+
const refs = [];
|
|
19
|
+
const vaultWriters = resolveVaultWriters(params.runtime, {
|
|
20
|
+
provider: params.provider,
|
|
21
|
+
accountId: params.accountIdForRef,
|
|
22
|
+
caller: params.caller
|
|
23
|
+
});
|
|
24
|
+
if (vaultWriters.length === 0) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`No durable connector credential store or vault writer is available for ${params.provider} account ${params.accountIdForRef}. Refusing to mark OAuth account connected without persisted credentials.`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!params.storageAccountId) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`No durable connector account id is available for ${params.provider} account ${params.accountIdForRef}. Refusing to mark OAuth account connected without persisted credential refs.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const storageWriters = resolveCredentialRefWriters(
|
|
35
|
+
params.runtime,
|
|
36
|
+
params.manager,
|
|
37
|
+
params.storageAccountId
|
|
38
|
+
);
|
|
39
|
+
if (storageWriters.length === 0) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`No durable connector credential ref writer is available for ${params.provider} account ${params.storageAccountId}. Refusing to mark OAuth account connected without persisted credential refs.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
for (const credential of params.credentials) {
|
|
45
|
+
const plannedRef = buildConnectorCredentialVaultRef({
|
|
46
|
+
agentId: nonEmptyString(params.runtime.agentId) ?? "agent",
|
|
47
|
+
provider: params.provider,
|
|
48
|
+
accountId: params.accountIdForRef,
|
|
49
|
+
credentialType: credential.credentialType
|
|
50
|
+
});
|
|
51
|
+
const vaultRef = await writeWithFirstAvailableVault(
|
|
52
|
+
vaultWriters,
|
|
53
|
+
plannedRef,
|
|
54
|
+
credential
|
|
55
|
+
);
|
|
56
|
+
refs.push({
|
|
57
|
+
credentialType: credential.credentialType,
|
|
58
|
+
vaultRef,
|
|
59
|
+
...credential.expiresAt !== void 0 ? { expiresAt: credential.expiresAt } : {},
|
|
60
|
+
...credential.metadata ? { metadata: credential.metadata } : {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (refs.length > 0) {
|
|
64
|
+
await writeRefsToStorage(storageWriters, refs);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
refs,
|
|
68
|
+
vaultAvailable: vaultWriters.length > 0,
|
|
69
|
+
storageAvailable: storageWriters.length > 0
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function loadConnectorOAuthAccessToken(params) {
|
|
73
|
+
const { records } = await loadConnectorCredentialRecords(params);
|
|
74
|
+
const tokenRecord = records.find(
|
|
75
|
+
(record) => sameCredentialType(record.credentialType, OAUTH_TOKENS_CREDENTIAL_TYPE)
|
|
76
|
+
);
|
|
77
|
+
if (!tokenRecord) return null;
|
|
78
|
+
const raw = nonEmptyString(tokenRecord.value) ?? (tokenRecord.vaultRef ? await readCredentialSecret(
|
|
79
|
+
params.runtime,
|
|
80
|
+
tokenRecord.vaultRef,
|
|
81
|
+
params.caller
|
|
82
|
+
) : void 0);
|
|
83
|
+
const parsed = raw ? parseMaybeJson(raw) : void 0;
|
|
84
|
+
const tokenSet = asRecord(parsed);
|
|
85
|
+
return nonEmptyString(tokenSet?.access_token) ?? null;
|
|
86
|
+
}
|
|
87
|
+
async function listConnectorAccounts(runtime, provider) {
|
|
88
|
+
try {
|
|
89
|
+
return await getConnectorAccountManager(runtime).listAccounts(provider);
|
|
90
|
+
} catch {
|
|
91
|
+
const storage = getService(
|
|
92
|
+
runtime,
|
|
93
|
+
CONNECTOR_ACCOUNT_STORAGE_SERVICE_TYPE
|
|
94
|
+
);
|
|
95
|
+
if (typeof storage?.listAccounts === "function") {
|
|
96
|
+
return storage.listAccounts(provider);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
function credentialRefRecordsFromMetadata(metadata) {
|
|
102
|
+
const record = asRecord(metadata);
|
|
103
|
+
if (!record) return [];
|
|
104
|
+
const oauth = asRecord(record.oauth);
|
|
105
|
+
return [
|
|
106
|
+
...credentialRefsFromUnknown(record.credentialRefs),
|
|
107
|
+
...credentialRefsFromUnknown(record.oauthCredentialRefs),
|
|
108
|
+
...credentialRefsFromUnknown(oauth?.credentialRefs)
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
async function loadConnectorCredentialRecords(params) {
|
|
112
|
+
const accounts = await listConnectorAccounts(params.runtime, params.provider);
|
|
113
|
+
const account = accounts.find(
|
|
114
|
+
(candidate) => candidate.id === params.accountId || candidate.externalId === params.accountId || candidate.displayHandle === params.accountId
|
|
115
|
+
);
|
|
116
|
+
if (!account) return { records: [] };
|
|
117
|
+
const records = [...credentialRefRecordsFromMetadata(account.metadata)];
|
|
118
|
+
for (const source of [
|
|
119
|
+
getService(params.runtime, CONNECTOR_ACCOUNT_STORAGE_SERVICE_TYPE),
|
|
120
|
+
params.runtime.adapter
|
|
121
|
+
]) {
|
|
122
|
+
const reader = source;
|
|
123
|
+
if (typeof reader?.listConnectorAccountCredentialRefs === "function") {
|
|
124
|
+
records.push(
|
|
125
|
+
...await reader.listConnectorAccountCredentialRefs({
|
|
126
|
+
accountId: account.id
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
} else if (typeof reader?.getConnectorAccountCredentialRef === "function") {
|
|
130
|
+
const ref = await reader.getConnectorAccountCredentialRef({
|
|
131
|
+
accountId: account.id,
|
|
132
|
+
credentialType: OAUTH_TOKENS_CREDENTIAL_TYPE
|
|
133
|
+
});
|
|
134
|
+
if (ref) records.push(ref);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { records };
|
|
138
|
+
}
|
|
139
|
+
function credentialRefsFromUnknown(value) {
|
|
140
|
+
if (Array.isArray(value)) {
|
|
141
|
+
return value.flatMap((entry) => {
|
|
142
|
+
const ref = credentialRefFromRecord(asRecord(entry));
|
|
143
|
+
return ref ? [ref] : [];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const record = asRecord(value);
|
|
147
|
+
if (!record) return [];
|
|
148
|
+
return Object.entries(record).flatMap(([credentialType, entry]) => {
|
|
149
|
+
const entryRecord = asRecord(entry);
|
|
150
|
+
if (entryRecord) {
|
|
151
|
+
const ref = credentialRefFromRecord({ credentialType, ...entryRecord });
|
|
152
|
+
return ref ? [ref] : [];
|
|
153
|
+
}
|
|
154
|
+
const vaultRef = nonEmptyString(entry);
|
|
155
|
+
return vaultRef ? [{ credentialType, vaultRef }] : [];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function credentialRefFromRecord(record) {
|
|
159
|
+
if (!record) return null;
|
|
160
|
+
const credentialType = nonEmptyString(
|
|
161
|
+
record.credentialType ?? record.type ?? record.name
|
|
162
|
+
);
|
|
163
|
+
const vaultRef = nonEmptyString(record.vaultRef ?? record.ref);
|
|
164
|
+
if (!credentialType || !vaultRef) return null;
|
|
165
|
+
return {
|
|
166
|
+
credentialType,
|
|
167
|
+
vaultRef,
|
|
168
|
+
metadata: asRecord(record.metadata) ?? null,
|
|
169
|
+
expiresAt: record.expiresAt,
|
|
170
|
+
updatedAt: record.updatedAt,
|
|
171
|
+
version: record.version ?? record.credentialVersion
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function readCredentialSecret(runtime, vaultRef, caller) {
|
|
175
|
+
for (const reader of resolveSecretReaders(runtime)) {
|
|
176
|
+
try {
|
|
177
|
+
const value = await readSecret(reader, vaultRef, caller, runtime);
|
|
178
|
+
const trimmed = nonEmptyString(value);
|
|
179
|
+
if (trimmed) return trimmed;
|
|
180
|
+
} catch {
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return void 0;
|
|
184
|
+
}
|
|
185
|
+
function resolveVaultWriters(runtime, context) {
|
|
186
|
+
const writers = [];
|
|
187
|
+
const credentialStore = getFirstService(runtime, [
|
|
188
|
+
"connector_credential_store",
|
|
189
|
+
"CONNECTOR_CREDENTIAL_STORE",
|
|
190
|
+
"connectorCredentialStore",
|
|
191
|
+
"credential_store"
|
|
192
|
+
]);
|
|
193
|
+
if (typeof credentialStore?.putSecret === "function") {
|
|
194
|
+
writers.push({
|
|
195
|
+
name: "connector_credential_store",
|
|
196
|
+
write: async (vaultRef, credential) => credentialStore.putSecret?.({
|
|
197
|
+
vaultRef,
|
|
198
|
+
agentId: nonEmptyString(runtime.agentId) ?? "agent",
|
|
199
|
+
provider: context.provider,
|
|
200
|
+
accountId: context.accountId,
|
|
201
|
+
credentialType: credential.credentialType,
|
|
202
|
+
value: credential.value,
|
|
203
|
+
caller: context.caller
|
|
204
|
+
}) ?? vaultRef
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const vault = getFirstService(runtime, ["vault", "VAULT"]);
|
|
208
|
+
if (typeof vault?.set === "function") {
|
|
209
|
+
writers.push({
|
|
210
|
+
name: "vault",
|
|
211
|
+
write: async (vaultRef, credential) => {
|
|
212
|
+
await vault.set?.(vaultRef, credential.value, {
|
|
213
|
+
sensitive: true,
|
|
214
|
+
caller: context.caller
|
|
215
|
+
});
|
|
216
|
+
return vaultRef;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const secrets = getService(runtime, "SECRETS");
|
|
221
|
+
if (typeof secrets?.setGlobal === "function" || typeof secrets?.set === "function") {
|
|
222
|
+
writers.push({
|
|
223
|
+
name: "SECRETS",
|
|
224
|
+
write: async (vaultRef, credential) => {
|
|
225
|
+
if (typeof secrets.setGlobal === "function") {
|
|
226
|
+
await secrets.setGlobal(vaultRef, credential.value, {
|
|
227
|
+
sensitive: true
|
|
228
|
+
});
|
|
229
|
+
return vaultRef;
|
|
230
|
+
}
|
|
231
|
+
await secrets.set?.(
|
|
232
|
+
vaultRef,
|
|
233
|
+
credential.value,
|
|
234
|
+
{ level: "global", agentId: runtime.agentId },
|
|
235
|
+
{ sensitive: true }
|
|
236
|
+
);
|
|
237
|
+
return vaultRef;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return writers;
|
|
242
|
+
}
|
|
243
|
+
function resolveSecretReaders(runtime) {
|
|
244
|
+
return [
|
|
245
|
+
getFirstService(runtime, [
|
|
246
|
+
"connector_credential_store",
|
|
247
|
+
"CONNECTOR_CREDENTIAL_STORE",
|
|
248
|
+
"connectorCredentialStore",
|
|
249
|
+
"credential_store"
|
|
250
|
+
]),
|
|
251
|
+
getFirstService(runtime, ["vault", "VAULT"]),
|
|
252
|
+
getService(runtime, "SECRETS")
|
|
253
|
+
].filter(Boolean);
|
|
254
|
+
}
|
|
255
|
+
async function readSecret(reader, vaultRef, caller, runtime) {
|
|
256
|
+
const candidate = reader;
|
|
257
|
+
if (typeof candidate.reveal === "function") {
|
|
258
|
+
return candidate.reveal(vaultRef, caller);
|
|
259
|
+
}
|
|
260
|
+
if (typeof candidate.get !== "function") return null;
|
|
261
|
+
if (reader && reader.constructor?.name === "SecretsService") {
|
|
262
|
+
return candidate.get(vaultRef, {
|
|
263
|
+
level: "global",
|
|
264
|
+
agentId: runtime.agentId
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return candidate.get(vaultRef, { reveal: true, caller });
|
|
268
|
+
}
|
|
269
|
+
function resolveCredentialRefWriters(runtime, manager, accountId) {
|
|
270
|
+
const candidates = [
|
|
271
|
+
manager?.getStorage?.(),
|
|
272
|
+
getService(runtime, CONNECTOR_ACCOUNT_STORAGE_SERVICE_TYPE),
|
|
273
|
+
runtime.adapter
|
|
274
|
+
].filter(Boolean);
|
|
275
|
+
const writers = [];
|
|
276
|
+
for (const candidate of candidates) {
|
|
277
|
+
const writer = candidate;
|
|
278
|
+
if (typeof writer.setConnectorAccountCredentialRef === "function") {
|
|
279
|
+
writers.push({
|
|
280
|
+
name: "setConnectorAccountCredentialRef",
|
|
281
|
+
write: async (ref) => {
|
|
282
|
+
await writer.setConnectorAccountCredentialRef?.({
|
|
283
|
+
accountId,
|
|
284
|
+
credentialType: ref.credentialType,
|
|
285
|
+
vaultRef: ref.vaultRef,
|
|
286
|
+
...ref.metadata ? { metadata: ref.metadata } : {},
|
|
287
|
+
...ref.expiresAt !== void 0 ? { expiresAt: ref.expiresAt } : {}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
} else if (typeof writer.setCredentialRef === "function") {
|
|
292
|
+
writers.push({
|
|
293
|
+
name: "setCredentialRef",
|
|
294
|
+
write: async (ref) => {
|
|
295
|
+
await writer.setCredentialRef?.({
|
|
296
|
+
accountId,
|
|
297
|
+
credentialType: ref.credentialType,
|
|
298
|
+
vaultRef: ref.vaultRef,
|
|
299
|
+
...ref.metadata ? { metadata: ref.metadata } : {},
|
|
300
|
+
...ref.expiresAt !== void 0 ? { expiresAt: ref.expiresAt } : {}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return writers;
|
|
307
|
+
}
|
|
308
|
+
async function writeWithFirstAvailableVault(writers, plannedRef, credential) {
|
|
309
|
+
const errors = [];
|
|
310
|
+
for (const writer of writers) {
|
|
311
|
+
try {
|
|
312
|
+
return await writer.write(plannedRef, credential);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
errors.push(
|
|
315
|
+
`${writer.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Failed to persist connector credential ref ${plannedRef}: ${errors.join("; ")}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
async function writeRefsToStorage(writers, refs) {
|
|
324
|
+
const errors = [];
|
|
325
|
+
for (const writer of writers) {
|
|
326
|
+
try {
|
|
327
|
+
for (const ref of refs) {
|
|
328
|
+
await writer.write(ref);
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
errors.push(
|
|
333
|
+
`${writer.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
throw new Error(
|
|
338
|
+
`Failed to persist connector credential refs: ${errors.join("; ")}`
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
function buildConnectorCredentialVaultRef(params) {
|
|
342
|
+
return [
|
|
343
|
+
"connector",
|
|
344
|
+
normalizeVaultSegment(params.agentId),
|
|
345
|
+
normalizeVaultSegment(params.provider),
|
|
346
|
+
normalizeVaultSegment(params.accountId),
|
|
347
|
+
normalizeVaultSegment(params.credentialType)
|
|
348
|
+
].join(".");
|
|
349
|
+
}
|
|
350
|
+
function normalizeVaultSegment(value) {
|
|
351
|
+
const normalized = value.trim().replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
352
|
+
return (normalized || "unknown").slice(0, 64);
|
|
353
|
+
}
|
|
354
|
+
function sameCredentialType(left, right) {
|
|
355
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
356
|
+
}
|
|
357
|
+
function parseMaybeJson(value) {
|
|
358
|
+
const trimmed = value.trim();
|
|
359
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return void 0;
|
|
360
|
+
try {
|
|
361
|
+
return JSON.parse(trimmed);
|
|
362
|
+
} catch {
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function getFirstService(runtime, serviceTypes) {
|
|
367
|
+
for (const serviceType of serviceTypes) {
|
|
368
|
+
const service = getService(runtime, serviceType);
|
|
369
|
+
if (service) return service;
|
|
370
|
+
}
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
function getService(runtime, serviceType) {
|
|
374
|
+
try {
|
|
375
|
+
return runtime.getService?.(serviceType) ?? null;
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function asRecord(value) {
|
|
381
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
382
|
+
}
|
|
383
|
+
function nonEmptyString(value) {
|
|
384
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// src/types.ts
|
|
388
|
+
var GITHUB_SERVICE_TYPE = "github";
|
|
389
|
+
var GitHubActions = {
|
|
390
|
+
GITHUB_ISSUE_OP: "GITHUB_ISSUE",
|
|
391
|
+
GITHUB_PR_OP: "GITHUB_PR",
|
|
392
|
+
GITHUB_NOTIFICATION_TRIAGE: "GITHUB_NOTIFICATION_TRIAGE"
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// src/accounts.ts
|
|
396
|
+
var DEFAULT_GITHUB_USER_ACCOUNT_ID = "user";
|
|
397
|
+
var DEFAULT_GITHUB_AGENT_ACCOUNT_ID = "agent";
|
|
398
|
+
function nonEmptyString2(value) {
|
|
399
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
400
|
+
}
|
|
401
|
+
function readSetting(runtime, key) {
|
|
402
|
+
return nonEmptyString2(runtime.getSetting(key));
|
|
403
|
+
}
|
|
404
|
+
function normalizeRole(value) {
|
|
405
|
+
return value === "user" || value === "agent" ? value : void 0;
|
|
406
|
+
}
|
|
407
|
+
function defaultGitHubAccountIdForRole(role) {
|
|
408
|
+
return role === "user" ? DEFAULT_GITHUB_USER_ACCOUNT_ID : DEFAULT_GITHUB_AGENT_ACCOUNT_ID;
|
|
409
|
+
}
|
|
410
|
+
function normalizeGitHubAccountId(value) {
|
|
411
|
+
return nonEmptyString2(value);
|
|
412
|
+
}
|
|
413
|
+
function resolveGitHubAccountSelection(options, defaultRole) {
|
|
414
|
+
const requestedAccountId = normalizeGitHubAccountId(options?.accountId);
|
|
415
|
+
const requestedRole = normalizeRole(options?.as);
|
|
416
|
+
return {
|
|
417
|
+
accountId: requestedAccountId,
|
|
418
|
+
role: requestedRole ?? normalizeRole(requestedAccountId) ?? defaultRole
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function parseAccountsJson(raw) {
|
|
422
|
+
if (!raw) return [];
|
|
423
|
+
try {
|
|
424
|
+
const parsed = JSON.parse(raw);
|
|
425
|
+
if (Array.isArray(parsed)) {
|
|
426
|
+
return parsed.filter(
|
|
427
|
+
(item) => Boolean(item) && typeof item === "object" && !Array.isArray(item)
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (parsed && typeof parsed === "object") {
|
|
431
|
+
return Object.entries(parsed).filter(([, value]) => value && typeof value === "object").map(([id, value]) => ({
|
|
432
|
+
...value,
|
|
433
|
+
accountId: value.accountId ?? id
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
function readRawField(record, keys) {
|
|
442
|
+
const credentials = record.credentials && typeof record.credentials === "object" ? record.credentials : {};
|
|
443
|
+
const settings = record.settings && typeof record.settings === "object" ? record.settings : {};
|
|
444
|
+
for (const source of [record, credentials, settings]) {
|
|
445
|
+
for (const key of keys) {
|
|
446
|
+
const value = nonEmptyString2(source[key]);
|
|
447
|
+
if (value) return value;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
function accountFromRecord(record) {
|
|
453
|
+
const accountId = normalizeGitHubAccountId(
|
|
454
|
+
record.accountId ?? record.id ?? record.name
|
|
455
|
+
);
|
|
456
|
+
const role = normalizeRole(record.role) ?? normalizeRole(record.as) ?? normalizeRole(accountId);
|
|
457
|
+
const token = readRawField(record, [
|
|
458
|
+
"GITHUB_PAT",
|
|
459
|
+
"GITHUB_TOKEN",
|
|
460
|
+
"GITHUB_ACCESS_TOKEN",
|
|
461
|
+
"token",
|
|
462
|
+
"pat",
|
|
463
|
+
"accessToken",
|
|
464
|
+
"access"
|
|
465
|
+
]);
|
|
466
|
+
if (!accountId || !role || !token) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
accountId,
|
|
471
|
+
role,
|
|
472
|
+
token,
|
|
473
|
+
label: nonEmptyString2(record.label ?? record.displayName)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function addAccount(accounts, account) {
|
|
477
|
+
if (account) {
|
|
478
|
+
accounts.set(account.accountId, account);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function readGitHubAccounts(runtime) {
|
|
482
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
483
|
+
const characterConfig = runtime.character?.settings?.github;
|
|
484
|
+
const characterAccounts = characterConfig?.accounts;
|
|
485
|
+
if (Array.isArray(characterAccounts)) {
|
|
486
|
+
for (const item of characterAccounts) {
|
|
487
|
+
if (item && typeof item === "object") {
|
|
488
|
+
addAccount(accounts, accountFromRecord(item));
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
} else if (characterAccounts && typeof characterAccounts === "object") {
|
|
492
|
+
for (const [id, value] of Object.entries(
|
|
493
|
+
characterAccounts
|
|
494
|
+
)) {
|
|
495
|
+
if (value && typeof value === "object") {
|
|
496
|
+
addAccount(
|
|
497
|
+
accounts,
|
|
498
|
+
accountFromRecord({
|
|
499
|
+
...value,
|
|
500
|
+
accountId: value.accountId ?? id
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const record of parseAccountsJson(
|
|
507
|
+
readSetting(runtime, "GITHUB_ACCOUNTS")
|
|
508
|
+
)) {
|
|
509
|
+
addAccount(accounts, accountFromRecord(record));
|
|
510
|
+
}
|
|
511
|
+
addAccount(
|
|
512
|
+
accounts,
|
|
513
|
+
legacyAccount(
|
|
514
|
+
runtime,
|
|
515
|
+
"user",
|
|
516
|
+
readSetting(runtime, "GITHUB_USER_ACCOUNT_ID") ?? DEFAULT_GITHUB_USER_ACCOUNT_ID,
|
|
517
|
+
"GITHUB_USER_PAT",
|
|
518
|
+
"ELIZA_E2E_GITHUB_USER_PAT"
|
|
519
|
+
)
|
|
520
|
+
);
|
|
521
|
+
addAccount(
|
|
522
|
+
accounts,
|
|
523
|
+
legacyAccount(
|
|
524
|
+
runtime,
|
|
525
|
+
"agent",
|
|
526
|
+
readSetting(runtime, "GITHUB_AGENT_ACCOUNT_ID") ?? DEFAULT_GITHUB_AGENT_ACCOUNT_ID,
|
|
527
|
+
"GITHUB_AGENT_PAT",
|
|
528
|
+
"ELIZA_E2E_GITHUB_AGENT_PAT"
|
|
529
|
+
)
|
|
530
|
+
);
|
|
531
|
+
return Array.from(accounts.values());
|
|
532
|
+
}
|
|
533
|
+
async function readGitHubAccountsWithConnectorCredentials(runtime) {
|
|
534
|
+
const accounts = /* @__PURE__ */ new Map();
|
|
535
|
+
for (const account of readGitHubAccounts(runtime)) {
|
|
536
|
+
accounts.set(account.accountId, account);
|
|
537
|
+
}
|
|
538
|
+
const connectorAccounts = await listConnectorAccounts(
|
|
539
|
+
runtime,
|
|
540
|
+
GITHUB_SERVICE_TYPE
|
|
541
|
+
);
|
|
542
|
+
for (const account of connectorAccounts) {
|
|
543
|
+
if (account.status !== "connected") continue;
|
|
544
|
+
const token = await loadConnectorOAuthAccessToken({
|
|
545
|
+
runtime,
|
|
546
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
547
|
+
accountId: account.id,
|
|
548
|
+
caller: "plugin-github"
|
|
549
|
+
});
|
|
550
|
+
if (!token) continue;
|
|
551
|
+
accounts.set(account.id, {
|
|
552
|
+
accountId: account.id,
|
|
553
|
+
role: connectorRoleToIdentity(account.role),
|
|
554
|
+
token,
|
|
555
|
+
label: account.label ?? account.displayHandle
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
return Array.from(accounts.values());
|
|
559
|
+
}
|
|
560
|
+
function legacyAccount(runtime, role, accountId, primaryKey, fallbackKey) {
|
|
561
|
+
const token = readSetting(runtime, primaryKey) ?? readSetting(runtime, fallbackKey);
|
|
562
|
+
if (!token) return null;
|
|
563
|
+
return { accountId, role, token };
|
|
564
|
+
}
|
|
565
|
+
function connectorRoleToIdentity(role) {
|
|
566
|
+
return typeof role === "string" && role.toUpperCase() === "AGENT" ? "agent" : "user";
|
|
567
|
+
}
|
|
568
|
+
function resolveGitHubAccount(accounts, selection) {
|
|
569
|
+
if (selection.accountId) {
|
|
570
|
+
const exact = accounts.find(
|
|
571
|
+
(account) => account.accountId === selection.accountId
|
|
572
|
+
);
|
|
573
|
+
if (exact) return exact;
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
const legacyId = defaultGitHubAccountIdForRole(selection.role);
|
|
577
|
+
return accounts.find((account) => account.accountId === legacyId) ?? accounts.find((account) => account.role === selection.role) ?? null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/action-helpers.ts
|
|
581
|
+
function getClient(runtime, selection) {
|
|
582
|
+
const service = runtime.getService(GITHUB_SERVICE_TYPE);
|
|
583
|
+
if (!service) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
return service.getOctokit(selection);
|
|
587
|
+
}
|
|
588
|
+
function requireString(options, key) {
|
|
589
|
+
const v = options?.[key];
|
|
590
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
591
|
+
}
|
|
592
|
+
function requireNumber(options, key) {
|
|
593
|
+
const v = options?.[key];
|
|
594
|
+
if (typeof v === "number" && Number.isInteger(v)) {
|
|
595
|
+
return v;
|
|
596
|
+
}
|
|
597
|
+
if (typeof v === "string" && /^\d+$/.test(v)) {
|
|
598
|
+
return Number(v);
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function requireStringArray(options, key) {
|
|
603
|
+
const v = options?.[key];
|
|
604
|
+
if (!Array.isArray(v)) {
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
const result = [];
|
|
608
|
+
for (const item of v) {
|
|
609
|
+
if (typeof item !== "string" || item.length === 0) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
result.push(item);
|
|
613
|
+
}
|
|
614
|
+
return result;
|
|
615
|
+
}
|
|
616
|
+
function optionalStringArray(options, key) {
|
|
617
|
+
const v = options?.[key];
|
|
618
|
+
if (v === void 0) {
|
|
619
|
+
return void 0;
|
|
620
|
+
}
|
|
621
|
+
return requireStringArray(options, key) ?? void 0;
|
|
622
|
+
}
|
|
623
|
+
function splitRepo(repo) {
|
|
624
|
+
const parts = repo.split("/");
|
|
625
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
return { owner: parts[0], name: parts[1] };
|
|
629
|
+
}
|
|
630
|
+
function needsClientError(selection) {
|
|
631
|
+
const accountSuffix = selection.accountId ? ` accountId "${selection.accountId}"` : ` ${selection.role} account`;
|
|
632
|
+
return `GitHub${accountSuffix} token not configured (set GITHUB_ACCOUNTS or ${selection.role === "user" ? "GITHUB_USER_PAT" : "GITHUB_AGENT_PAT"})`;
|
|
633
|
+
}
|
|
634
|
+
function getServiceOrNull(runtime) {
|
|
635
|
+
return runtime.getService(GITHUB_SERVICE_TYPE);
|
|
636
|
+
}
|
|
637
|
+
function buildResolvedClient(runtime, selection) {
|
|
638
|
+
if (!getServiceOrNull(runtime)) {
|
|
639
|
+
return { error: "GitHub service not available" };
|
|
640
|
+
}
|
|
641
|
+
const resolvedSelection = typeof selection === "string" ? { role: selection } : selection;
|
|
642
|
+
const client = getClient(runtime, resolvedSelection);
|
|
643
|
+
if (!client) {
|
|
644
|
+
return { error: needsClientError(resolvedSelection) };
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
client,
|
|
648
|
+
identity: resolvedSelection.role,
|
|
649
|
+
accountId: resolvedSelection.accountId
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function resolveAccountSelection(options, defaultIdentity) {
|
|
653
|
+
return resolveGitHubAccountSelection(options, defaultIdentity);
|
|
654
|
+
}
|
|
655
|
+
function describeSelection(selection) {
|
|
656
|
+
return selection.accountId ? `${selection.role} (${selection.accountId})` : selection.role;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/rate-limit.ts
|
|
660
|
+
function toErrorLike(value) {
|
|
661
|
+
if (typeof value !== "object" || value === null) {
|
|
662
|
+
return { message: String(value) };
|
|
663
|
+
}
|
|
664
|
+
return value;
|
|
665
|
+
}
|
|
666
|
+
function headerNumber(headers, name) {
|
|
667
|
+
if (!headers) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const raw = headers[name] ?? headers[name.toLowerCase()];
|
|
671
|
+
if (raw === void 0) {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const num = typeof raw === "number" ? raw : Number(raw);
|
|
675
|
+
return Number.isFinite(num) ? num : null;
|
|
676
|
+
}
|
|
677
|
+
function inspectRateLimit(err) {
|
|
678
|
+
const e = toErrorLike(err);
|
|
679
|
+
const headers = e.response?.headers;
|
|
680
|
+
const remaining = headerNumber(headers, "x-ratelimit-remaining");
|
|
681
|
+
const resetSeconds = headerNumber(headers, "x-ratelimit-reset");
|
|
682
|
+
const isRateLimited = e.status === 403 && remaining === 0;
|
|
683
|
+
return {
|
|
684
|
+
isRateLimited,
|
|
685
|
+
remaining,
|
|
686
|
+
resetAtMs: resetSeconds === null ? null : resetSeconds * 1e3
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function formatRateLimitMessage(details) {
|
|
690
|
+
if (!details.isRateLimited) {
|
|
691
|
+
return "GitHub request failed";
|
|
692
|
+
}
|
|
693
|
+
if (details.resetAtMs === null) {
|
|
694
|
+
return "GitHub rate limit exhausted";
|
|
695
|
+
}
|
|
696
|
+
const reset = new Date(details.resetAtMs).toISOString();
|
|
697
|
+
return `GitHub rate limit exhausted; resets at ${reset}`;
|
|
698
|
+
}
|
|
699
|
+
function errorMessage(err) {
|
|
700
|
+
if (err instanceof Error) {
|
|
701
|
+
return err.message;
|
|
702
|
+
}
|
|
703
|
+
if (typeof err === "string") {
|
|
704
|
+
return err;
|
|
705
|
+
}
|
|
706
|
+
const e = toErrorLike(err);
|
|
707
|
+
return e.message ?? "unknown error";
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/actions/issue-op.ts
|
|
711
|
+
var SUPPORTED_OPS = /* @__PURE__ */ new Set([
|
|
712
|
+
"create",
|
|
713
|
+
"assign",
|
|
714
|
+
"close",
|
|
715
|
+
"reopen",
|
|
716
|
+
"comment",
|
|
717
|
+
"label"
|
|
718
|
+
]);
|
|
719
|
+
function parseOp(value) {
|
|
720
|
+
if (typeof value !== "string") return null;
|
|
721
|
+
return SUPPORTED_OPS.has(value) ? value : null;
|
|
722
|
+
}
|
|
723
|
+
function describeOp(op) {
|
|
724
|
+
switch (op) {
|
|
725
|
+
case "create":
|
|
726
|
+
return "create";
|
|
727
|
+
case "assign":
|
|
728
|
+
return "assign";
|
|
729
|
+
case "close":
|
|
730
|
+
return "close";
|
|
731
|
+
case "reopen":
|
|
732
|
+
return "reopen";
|
|
733
|
+
case "comment":
|
|
734
|
+
return "comment on";
|
|
735
|
+
case "label":
|
|
736
|
+
return "label";
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function runCreate(resolved, parts, repo, options, callback) {
|
|
740
|
+
const title = requireString(options, "title");
|
|
741
|
+
const body = requireString(options, "body");
|
|
742
|
+
const labels = optionalStringArray(options, "labels");
|
|
743
|
+
const assignees = optionalStringArray(options, "assignees");
|
|
744
|
+
if (!title) {
|
|
745
|
+
const err = "GITHUB_ISSUE_OP create requires title";
|
|
746
|
+
await callback?.({ text: err });
|
|
747
|
+
return { success: false, error: err };
|
|
748
|
+
}
|
|
749
|
+
const resp = await resolved.client.issues.create({
|
|
750
|
+
owner: parts.owner,
|
|
751
|
+
repo: parts.name,
|
|
752
|
+
title,
|
|
753
|
+
body: body ?? void 0,
|
|
754
|
+
labels,
|
|
755
|
+
assignees
|
|
756
|
+
});
|
|
757
|
+
await callback?.({
|
|
758
|
+
text: `Created issue ${repo}#${resp.data.number}: ${resp.data.html_url}`
|
|
759
|
+
});
|
|
760
|
+
return {
|
|
761
|
+
success: true,
|
|
762
|
+
data: {
|
|
763
|
+
op: "create",
|
|
764
|
+
number: resp.data.number,
|
|
765
|
+
url: resp.data.html_url
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
async function runAssign(resolved, parts, repo, options, callback) {
|
|
770
|
+
const number = requireNumber(options, "number");
|
|
771
|
+
const assignees = requireStringArray(options, "assignees");
|
|
772
|
+
if (!number || !assignees || assignees.length === 0) {
|
|
773
|
+
const err = "GITHUB_ISSUE_OP assign requires number (integer) and assignees (non-empty string[])";
|
|
774
|
+
await callback?.({ text: err });
|
|
775
|
+
return { success: false, error: err };
|
|
776
|
+
}
|
|
777
|
+
const resp = await resolved.client.issues.addAssignees({
|
|
778
|
+
owner: parts.owner,
|
|
779
|
+
repo: parts.name,
|
|
780
|
+
issue_number: number,
|
|
781
|
+
assignees
|
|
782
|
+
});
|
|
783
|
+
const actual = (resp.data.assignees ?? []).map((a) => a?.login).filter((x) => typeof x === "string");
|
|
784
|
+
await callback?.({
|
|
785
|
+
text: `Assigned [${actual.join(", ")}] to ${repo}#${number}`
|
|
786
|
+
});
|
|
787
|
+
return {
|
|
788
|
+
success: true,
|
|
789
|
+
data: { op: "assign", number, assignees: actual }
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async function runStateChange(resolved, parts, repo, options, callback, target) {
|
|
793
|
+
const number = requireNumber(options, "number");
|
|
794
|
+
if (!number) {
|
|
795
|
+
const err = `GITHUB_ISSUE_OP ${target === "closed" ? "close" : "reopen"} requires number (integer)`;
|
|
796
|
+
await callback?.({ text: err });
|
|
797
|
+
return { success: false, error: err };
|
|
798
|
+
}
|
|
799
|
+
const resp = await resolved.client.issues.update({
|
|
800
|
+
owner: parts.owner,
|
|
801
|
+
repo: parts.name,
|
|
802
|
+
issue_number: number,
|
|
803
|
+
state: target
|
|
804
|
+
});
|
|
805
|
+
const verb = target === "closed" ? "Closed" : "Reopened";
|
|
806
|
+
await callback?.({ text: `${verb} ${repo}#${number}: ${resp.data.title}` });
|
|
807
|
+
return {
|
|
808
|
+
success: true,
|
|
809
|
+
data: {
|
|
810
|
+
op: target === "closed" ? "close" : "reopen",
|
|
811
|
+
number,
|
|
812
|
+
title: resp.data.title
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
async function runComment(resolved, parts, repo, options, callback) {
|
|
817
|
+
const number = requireNumber(options, "number");
|
|
818
|
+
const body = requireString(options, "body");
|
|
819
|
+
if (!number || !body) {
|
|
820
|
+
const err = "GITHUB_ISSUE_OP comment requires number (integer) and body";
|
|
821
|
+
await callback?.({ text: err });
|
|
822
|
+
return { success: false, error: err };
|
|
823
|
+
}
|
|
824
|
+
const resp = await resolved.client.issues.createComment({
|
|
825
|
+
owner: parts.owner,
|
|
826
|
+
repo: parts.name,
|
|
827
|
+
issue_number: number,
|
|
828
|
+
body
|
|
829
|
+
});
|
|
830
|
+
await callback?.({
|
|
831
|
+
text: `Commented on ${repo}#${number}: ${resp.data.html_url}`
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
success: true,
|
|
835
|
+
data: {
|
|
836
|
+
op: "comment",
|
|
837
|
+
number,
|
|
838
|
+
commentId: resp.data.id,
|
|
839
|
+
url: resp.data.html_url
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
async function runLabel(resolved, parts, repo, options, callback) {
|
|
844
|
+
const number = requireNumber(options, "number");
|
|
845
|
+
const labels = requireStringArray(options, "labels");
|
|
846
|
+
if (!number || !labels || labels.length === 0) {
|
|
847
|
+
const err = "GITHUB_ISSUE_OP label requires number (integer) and labels (non-empty string[])";
|
|
848
|
+
await callback?.({ text: err });
|
|
849
|
+
return { success: false, error: err };
|
|
850
|
+
}
|
|
851
|
+
const resp = await resolved.client.issues.addLabels({
|
|
852
|
+
owner: parts.owner,
|
|
853
|
+
repo: parts.name,
|
|
854
|
+
issue_number: number,
|
|
855
|
+
labels
|
|
856
|
+
});
|
|
857
|
+
const applied = (resp.data ?? []).map((label) => typeof label === "string" ? label : label?.name ?? null).filter((x) => typeof x === "string");
|
|
858
|
+
await callback?.({
|
|
859
|
+
text: `Applied labels [${applied.join(", ")}] to ${repo}#${number}`
|
|
860
|
+
});
|
|
861
|
+
return {
|
|
862
|
+
success: true,
|
|
863
|
+
data: { op: "label", number, labels: applied }
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
function buildPreview(op, repo, identity, options) {
|
|
867
|
+
const number = requireNumber(options, "number");
|
|
868
|
+
const title = requireString(options, "title");
|
|
869
|
+
const body = requireString(options, "body");
|
|
870
|
+
const assignees = optionalStringArray(options, "assignees");
|
|
871
|
+
const labels = optionalStringArray(options, "labels");
|
|
872
|
+
const head = `About to ${describeOp(op)} ${repo}`;
|
|
873
|
+
const target = number ? `#${number}` : "";
|
|
874
|
+
const detail = (() => {
|
|
875
|
+
switch (op) {
|
|
876
|
+
case "create":
|
|
877
|
+
return ` issue: "${title ?? "(no title)"}"${labels ? ` [labels: ${labels.join(", ")}]` : ""}${assignees ? ` [assignees: ${assignees.join(", ")}]` : ""}`;
|
|
878
|
+
case "assign":
|
|
879
|
+
return ` with [${assignees?.join(", ") ?? ""}]`;
|
|
880
|
+
case "label":
|
|
881
|
+
return ` with [${labels?.join(", ") ?? ""}]`;
|
|
882
|
+
case "comment":
|
|
883
|
+
return body ? ` body: "${body.slice(0, 120)}"` : "";
|
|
884
|
+
default:
|
|
885
|
+
return "";
|
|
886
|
+
}
|
|
887
|
+
})();
|
|
888
|
+
return `${head}${target}${detail} as ${identity}. Re-invoke with confirmed: true to proceed.`;
|
|
889
|
+
}
|
|
890
|
+
var issueOpAction = {
|
|
891
|
+
name: GitHubActions.GITHUB_ISSUE_OP,
|
|
892
|
+
contexts: ["code", "tasks", "connectors", "automation"],
|
|
893
|
+
contextGate: { anyOf: ["code", "tasks", "connectors", "automation"] },
|
|
894
|
+
roleGate: { minRole: "USER" },
|
|
895
|
+
similes: [
|
|
896
|
+
"CREATE_ISSUE",
|
|
897
|
+
"OPEN_ISSUE",
|
|
898
|
+
"FILE_ISSUE",
|
|
899
|
+
"GITHUB_CREATE_ISSUE",
|
|
900
|
+
"ASSIGN_ISSUE",
|
|
901
|
+
"ASSIGN_GITHUB_ISSUE",
|
|
902
|
+
"ADD_ASSIGNEE",
|
|
903
|
+
"CLOSE_ISSUE",
|
|
904
|
+
"REOPEN_ISSUE",
|
|
905
|
+
"COMMENT_ISSUE",
|
|
906
|
+
"ADD_ISSUE_COMMENT",
|
|
907
|
+
"LABEL_ISSUE",
|
|
908
|
+
"ADD_ISSUE_LABEL",
|
|
909
|
+
"MANAGE_ISSUES"
|
|
910
|
+
],
|
|
911
|
+
description: "Single router for GitHub issue ops: create, assign, close, reopen, comment, label. Requires confirmed:true.",
|
|
912
|
+
descriptionCompressed: "GitHub issue ops: create, assign, close, reopen, comment, label.",
|
|
913
|
+
parameters: [
|
|
914
|
+
{
|
|
915
|
+
name: "subaction",
|
|
916
|
+
description: "Issue operation: create, assign, close, reopen, comment, or label.",
|
|
917
|
+
required: true,
|
|
918
|
+
schema: { type: "string", enum: [...SUPPORTED_OPS] }
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
name: "repo",
|
|
922
|
+
description: "Repository in owner/name form.",
|
|
923
|
+
required: true,
|
|
924
|
+
schema: { type: "string" }
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
name: "number",
|
|
928
|
+
description: "Issue number for existing-issue operations.",
|
|
929
|
+
required: false,
|
|
930
|
+
schema: { type: "number" }
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: "title",
|
|
934
|
+
description: "Issue title for create.",
|
|
935
|
+
required: false,
|
|
936
|
+
schema: { type: "string" }
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "body",
|
|
940
|
+
description: "Issue body or comment body.",
|
|
941
|
+
required: false,
|
|
942
|
+
schema: { type: "string" }
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: "assignees",
|
|
946
|
+
description: "GitHub usernames to assign.",
|
|
947
|
+
required: false,
|
|
948
|
+
schema: { type: "array", items: { type: "string" } }
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
name: "labels",
|
|
952
|
+
description: "Labels to apply on create or label.",
|
|
953
|
+
required: false,
|
|
954
|
+
schema: { type: "array", items: { type: "string" } }
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
name: "as",
|
|
958
|
+
description: "Identity to use: agent or user.",
|
|
959
|
+
required: false,
|
|
960
|
+
schema: { type: "string", enum: ["agent", "user"], default: "agent" }
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
name: "accountId",
|
|
964
|
+
description: "Optional GitHub account id from GITHUB_ACCOUNTS. Defaults by role.",
|
|
965
|
+
required: false,
|
|
966
|
+
schema: { type: "string" }
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: "confirmed",
|
|
970
|
+
description: "Must be true to perform the write operation.",
|
|
971
|
+
required: false,
|
|
972
|
+
schema: { type: "boolean", default: false }
|
|
973
|
+
}
|
|
974
|
+
],
|
|
975
|
+
validate: async (runtime, _message) => {
|
|
976
|
+
const r = buildResolvedClient(runtime, "agent");
|
|
977
|
+
return !("error" in r);
|
|
978
|
+
},
|
|
979
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
980
|
+
const op = parseOp(options?.op);
|
|
981
|
+
if (!op) {
|
|
982
|
+
const err = "GITHUB_ISSUE_OP requires op (create|assign|close|reopen|comment|label)";
|
|
983
|
+
await callback?.({ text: err });
|
|
984
|
+
return { success: false, error: err };
|
|
985
|
+
}
|
|
986
|
+
const selection = resolveAccountSelection(options, "agent");
|
|
987
|
+
const repo = requireString(options, "repo");
|
|
988
|
+
if (!repo) {
|
|
989
|
+
const err = "GITHUB_ISSUE_OP requires repo (owner/name)";
|
|
990
|
+
await callback?.({ text: err });
|
|
991
|
+
return { success: false, error: err };
|
|
992
|
+
}
|
|
993
|
+
const parts = splitRepo(repo);
|
|
994
|
+
if (!parts) {
|
|
995
|
+
const err = `Invalid repo "${repo}" \u2014 expected "owner/name"`;
|
|
996
|
+
await callback?.({ text: err });
|
|
997
|
+
return { success: false, error: err };
|
|
998
|
+
}
|
|
999
|
+
const preview = buildPreview(
|
|
1000
|
+
op,
|
|
1001
|
+
repo,
|
|
1002
|
+
describeSelection(selection),
|
|
1003
|
+
options
|
|
1004
|
+
);
|
|
1005
|
+
const decision = await requireConfirmation({
|
|
1006
|
+
runtime,
|
|
1007
|
+
message,
|
|
1008
|
+
actionName: "GITHUB_ISSUE_OP",
|
|
1009
|
+
pendingKey: `${op}:${repo}`,
|
|
1010
|
+
prompt: `${preview} Reply yes to confirm or no to cancel.`,
|
|
1011
|
+
callback
|
|
1012
|
+
});
|
|
1013
|
+
if (decision.status === "pending") {
|
|
1014
|
+
return {
|
|
1015
|
+
success: false,
|
|
1016
|
+
requiresConfirmation: true,
|
|
1017
|
+
preview
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (decision.status === "cancelled") {
|
|
1021
|
+
const cancelMessage = "GitHub issue operation cancelled.";
|
|
1022
|
+
await callback?.({ text: cancelMessage });
|
|
1023
|
+
return { success: false, error: cancelMessage };
|
|
1024
|
+
}
|
|
1025
|
+
const resolved = buildResolvedClient(runtime, selection);
|
|
1026
|
+
if ("error" in resolved) {
|
|
1027
|
+
await callback?.({ text: resolved.error });
|
|
1028
|
+
return { success: false, error: resolved.error };
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
switch (op) {
|
|
1032
|
+
case "create":
|
|
1033
|
+
return await runCreate(resolved, parts, repo, options, callback);
|
|
1034
|
+
case "assign":
|
|
1035
|
+
return await runAssign(resolved, parts, repo, options, callback);
|
|
1036
|
+
case "close":
|
|
1037
|
+
return await runStateChange(
|
|
1038
|
+
resolved,
|
|
1039
|
+
parts,
|
|
1040
|
+
repo,
|
|
1041
|
+
options,
|
|
1042
|
+
callback,
|
|
1043
|
+
"closed"
|
|
1044
|
+
);
|
|
1045
|
+
case "reopen":
|
|
1046
|
+
return await runStateChange(
|
|
1047
|
+
resolved,
|
|
1048
|
+
parts,
|
|
1049
|
+
repo,
|
|
1050
|
+
options,
|
|
1051
|
+
callback,
|
|
1052
|
+
"open"
|
|
1053
|
+
);
|
|
1054
|
+
case "comment":
|
|
1055
|
+
return await runComment(resolved, parts, repo, options, callback);
|
|
1056
|
+
case "label":
|
|
1057
|
+
return await runLabel(resolved, parts, repo, options, callback);
|
|
1058
|
+
}
|
|
1059
|
+
} catch (err) {
|
|
1060
|
+
const rl = inspectRateLimit(err);
|
|
1061
|
+
const message2 = rl.isRateLimited ? formatRateLimitMessage(rl) : `GITHUB_ISSUE_OP ${op} failed: ${errorMessage(err)}`;
|
|
1062
|
+
logger.warn({ message: message2 }, "[GitHub:GITHUB_ISSUE_OP]");
|
|
1063
|
+
await callback?.({ text: message2 });
|
|
1064
|
+
return { success: false, error: message2 };
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
examples: [
|
|
1068
|
+
[
|
|
1069
|
+
{
|
|
1070
|
+
name: "{{user1}}",
|
|
1071
|
+
content: {
|
|
1072
|
+
text: "Open an issue in elizaOS/eliza titled 'Docs gap'"
|
|
1073
|
+
}
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
name: "{{agentName}}",
|
|
1077
|
+
content: {
|
|
1078
|
+
text: "Created issue elizaOS/eliza#101"
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
],
|
|
1082
|
+
[
|
|
1083
|
+
{
|
|
1084
|
+
name: "{{user1}}",
|
|
1085
|
+
content: {
|
|
1086
|
+
text: "Close issue elizaOS/eliza#42"
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
name: "{{agentName}}",
|
|
1091
|
+
content: {
|
|
1092
|
+
text: "Closed elizaOS/eliza#42"
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
]
|
|
1096
|
+
]
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
// src/actions/notification-triage.ts
|
|
1100
|
+
import { logger as logger2 } from "@elizaos/core";
|
|
1101
|
+
var REASON_SCORES = {
|
|
1102
|
+
security_advisory: 100,
|
|
1103
|
+
team_mention: 70,
|
|
1104
|
+
author: 60,
|
|
1105
|
+
mention: 55,
|
|
1106
|
+
assign: 50,
|
|
1107
|
+
review_requested: 80,
|
|
1108
|
+
state_change: 20,
|
|
1109
|
+
comment: 30,
|
|
1110
|
+
subscribed: 10,
|
|
1111
|
+
manual: 15,
|
|
1112
|
+
invitation: 40,
|
|
1113
|
+
ci_activity: 25
|
|
1114
|
+
};
|
|
1115
|
+
var SUBJECT_TYPE_SCORES = {
|
|
1116
|
+
PullRequest: 20,
|
|
1117
|
+
Issue: 15,
|
|
1118
|
+
Release: 10,
|
|
1119
|
+
Commit: 5,
|
|
1120
|
+
Discussion: 8
|
|
1121
|
+
};
|
|
1122
|
+
var NOTIFICATION_TRIAGE_LIMIT = 25;
|
|
1123
|
+
function scoreNotification(params) {
|
|
1124
|
+
const base = REASON_SCORES[params.reason] ?? 10;
|
|
1125
|
+
const subject = SUBJECT_TYPE_SCORES[params.subjectType] ?? 0;
|
|
1126
|
+
let freshness = 0;
|
|
1127
|
+
if (params.repoPushedAtMs !== null) {
|
|
1128
|
+
const ageHours = (params.nowMs - params.repoPushedAtMs) / (1e3 * 60 * 60);
|
|
1129
|
+
if (ageHours < 1) freshness = 20;
|
|
1130
|
+
else if (ageHours < 6) freshness = 15;
|
|
1131
|
+
else if (ageHours < 24) freshness = 10;
|
|
1132
|
+
else if (ageHours < 24 * 7) freshness = 5;
|
|
1133
|
+
}
|
|
1134
|
+
return base + subject + freshness;
|
|
1135
|
+
}
|
|
1136
|
+
var notificationTriageAction = {
|
|
1137
|
+
name: GitHubActions.GITHUB_NOTIFICATION_TRIAGE,
|
|
1138
|
+
contexts: ["code", "tasks", "connectors", "automation"],
|
|
1139
|
+
contextGate: { anyOf: ["code", "tasks", "connectors", "automation"] },
|
|
1140
|
+
roleGate: { minRole: "USER" },
|
|
1141
|
+
similes: ["TRIAGE_GITHUB_NOTIFICATIONS", "GITHUB_INBOX"],
|
|
1142
|
+
description: "Returns unread GitHub notifications sorted by a priority score derived from reason, subject type, and repo freshness.",
|
|
1143
|
+
descriptionCompressed: "unread GitHub notifications sorted by reason|subject|repo freshness",
|
|
1144
|
+
parameters: [
|
|
1145
|
+
{
|
|
1146
|
+
name: "as",
|
|
1147
|
+
description: "Identity to use when reading notifications: user or agent.",
|
|
1148
|
+
required: false,
|
|
1149
|
+
schema: { type: "string", enum: ["user", "agent"], default: "user" }
|
|
1150
|
+
},
|
|
1151
|
+
{
|
|
1152
|
+
name: "accountId",
|
|
1153
|
+
description: "Optional GitHub account id from GITHUB_ACCOUNTS. Defaults by role.",
|
|
1154
|
+
required: false,
|
|
1155
|
+
schema: { type: "string" }
|
|
1156
|
+
}
|
|
1157
|
+
],
|
|
1158
|
+
validate: async (runtime, _message) => {
|
|
1159
|
+
const r = buildResolvedClient(runtime, "user");
|
|
1160
|
+
return !("error" in r);
|
|
1161
|
+
},
|
|
1162
|
+
handler: async (runtime, _message, _state, options, callback) => {
|
|
1163
|
+
const selection = resolveAccountSelection(options, "user");
|
|
1164
|
+
const resolved = buildResolvedClient(runtime, selection);
|
|
1165
|
+
if ("error" in resolved) {
|
|
1166
|
+
await callback?.({ text: resolved.error });
|
|
1167
|
+
return { success: false, error: resolved.error };
|
|
1168
|
+
}
|
|
1169
|
+
try {
|
|
1170
|
+
const resp = await resolved.client.activity.listNotificationsForAuthenticatedUser({
|
|
1171
|
+
all: false,
|
|
1172
|
+
per_page: 50
|
|
1173
|
+
});
|
|
1174
|
+
const notifications = resp.data;
|
|
1175
|
+
const nowMs = Date.now();
|
|
1176
|
+
const triaged = notifications.map((n) => {
|
|
1177
|
+
const repoPushedAt = n.repository?.pushed_at ?? null;
|
|
1178
|
+
const repoPushedAtMs = typeof repoPushedAt === "string" ? Date.parse(repoPushedAt) : null;
|
|
1179
|
+
const reason = typeof n.reason === "string" ? n.reason : "unknown";
|
|
1180
|
+
const subjectType = typeof n.subject?.type === "string" ? n.subject.type : "Unknown";
|
|
1181
|
+
return {
|
|
1182
|
+
id: n.id,
|
|
1183
|
+
reason,
|
|
1184
|
+
repo: n.repository?.full_name ?? "unknown",
|
|
1185
|
+
title: n.subject?.title ?? "(untitled)",
|
|
1186
|
+
subjectType,
|
|
1187
|
+
url: n.subject?.url ?? null,
|
|
1188
|
+
updatedAt: n.updated_at,
|
|
1189
|
+
score: scoreNotification({
|
|
1190
|
+
reason,
|
|
1191
|
+
subjectType,
|
|
1192
|
+
repoPushedAtMs: repoPushedAtMs !== null && Number.isFinite(repoPushedAtMs) ? repoPushedAtMs : null,
|
|
1193
|
+
nowMs
|
|
1194
|
+
})
|
|
1195
|
+
};
|
|
1196
|
+
});
|
|
1197
|
+
triaged.sort((a, b) => b.score - a.score);
|
|
1198
|
+
const boundedTriaged = triaged.slice(0, NOTIFICATION_TRIAGE_LIMIT);
|
|
1199
|
+
await callback?.({
|
|
1200
|
+
text: `Triaged ${boundedTriaged.length} unread notification(s)`
|
|
1201
|
+
});
|
|
1202
|
+
return {
|
|
1203
|
+
success: true,
|
|
1204
|
+
data: {
|
|
1205
|
+
notifications: boundedTriaged,
|
|
1206
|
+
notificationLimit: NOTIFICATION_TRIAGE_LIMIT,
|
|
1207
|
+
totalUnread: triaged.length
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
const rl = inspectRateLimit(err);
|
|
1212
|
+
const message = rl.isRateLimited ? formatRateLimitMessage(rl) : `GITHUB_NOTIFICATION_TRIAGE failed: ${errorMessage(err)}`;
|
|
1213
|
+
logger2.warn({ message }, "[GitHub:GITHUB_NOTIFICATION_TRIAGE]");
|
|
1214
|
+
await callback?.({ text: message });
|
|
1215
|
+
return { success: false, error: message };
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
examples: [
|
|
1219
|
+
[
|
|
1220
|
+
{
|
|
1221
|
+
name: "{{user1}}",
|
|
1222
|
+
content: { text: "What's in my GitHub inbox?" }
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
name: "{{agentName}}",
|
|
1226
|
+
content: { text: "Triaged 7 unread notification(s)" }
|
|
1227
|
+
}
|
|
1228
|
+
]
|
|
1229
|
+
]
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// src/actions/pr-op.ts
|
|
1233
|
+
import { logger as logger3, requireConfirmation as requireConfirmation2 } from "@elizaos/core";
|
|
1234
|
+
var SUPPORTED_OPS2 = /* @__PURE__ */ new Set(["list", "review"]);
|
|
1235
|
+
var EVENT_BY_ACTION = {
|
|
1236
|
+
approve: "APPROVE",
|
|
1237
|
+
"request-changes": "REQUEST_CHANGES",
|
|
1238
|
+
comment: "COMMENT"
|
|
1239
|
+
};
|
|
1240
|
+
function parseOp2(value) {
|
|
1241
|
+
if (typeof value !== "string") return null;
|
|
1242
|
+
return SUPPORTED_OPS2.has(value) ? value : null;
|
|
1243
|
+
}
|
|
1244
|
+
function parseState(value) {
|
|
1245
|
+
return value === "closed" || value === "all" ? value : "open";
|
|
1246
|
+
}
|
|
1247
|
+
function parseReviewAction(value) {
|
|
1248
|
+
return value === "approve" || value === "request-changes" || value === "comment" ? value : null;
|
|
1249
|
+
}
|
|
1250
|
+
async function runList(runtime, options, callback) {
|
|
1251
|
+
const selection = resolveAccountSelection(options, "agent");
|
|
1252
|
+
const resolved = buildResolvedClient(runtime, selection);
|
|
1253
|
+
if ("error" in resolved) {
|
|
1254
|
+
await callback?.({ text: resolved.error });
|
|
1255
|
+
return { success: false, error: resolved.error };
|
|
1256
|
+
}
|
|
1257
|
+
const state = parseState(options?.state);
|
|
1258
|
+
const author = requireString(options, "author");
|
|
1259
|
+
const repo = requireString(options, "repo");
|
|
1260
|
+
const prs = [];
|
|
1261
|
+
if (repo) {
|
|
1262
|
+
const parts = splitRepo(repo);
|
|
1263
|
+
if (!parts) {
|
|
1264
|
+
const err = `Invalid repo "${repo}" \u2014 expected "owner/name"`;
|
|
1265
|
+
await callback?.({ text: err });
|
|
1266
|
+
return { success: false, error: err };
|
|
1267
|
+
}
|
|
1268
|
+
const resp = await resolved.client.pulls.list({
|
|
1269
|
+
owner: parts.owner,
|
|
1270
|
+
repo: parts.name,
|
|
1271
|
+
state,
|
|
1272
|
+
per_page: 100
|
|
1273
|
+
});
|
|
1274
|
+
for (const pr of resp.data) {
|
|
1275
|
+
if (author && pr.user?.login !== author) {
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
prs.push({
|
|
1279
|
+
repo,
|
|
1280
|
+
number: pr.number,
|
|
1281
|
+
title: pr.title,
|
|
1282
|
+
author: pr.user?.login ?? null,
|
|
1283
|
+
state: pr.state,
|
|
1284
|
+
url: pr.html_url
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
} else {
|
|
1288
|
+
const q = [
|
|
1289
|
+
"is:pr",
|
|
1290
|
+
state === "all" ? "" : `is:${state}`,
|
|
1291
|
+
author ? `author:${author}` : ""
|
|
1292
|
+
].filter(Boolean).join(" ");
|
|
1293
|
+
const resp = await resolved.client.search.issuesAndPullRequests({
|
|
1294
|
+
q,
|
|
1295
|
+
per_page: 50
|
|
1296
|
+
});
|
|
1297
|
+
for (const item of resp.data.items) {
|
|
1298
|
+
const match = /\/repos\/([^/]+\/[^/]+)(?:\/|$)/.exec(item.repository_url);
|
|
1299
|
+
const repoName = match?.[1] ?? item.repository_url;
|
|
1300
|
+
prs.push({
|
|
1301
|
+
repo: repoName,
|
|
1302
|
+
number: item.number,
|
|
1303
|
+
title: item.title,
|
|
1304
|
+
author: item.user?.login ?? null,
|
|
1305
|
+
state: item.state,
|
|
1306
|
+
url: item.html_url
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
await callback?.({ text: `Found ${prs.length} pull request(s)` });
|
|
1311
|
+
return { success: true, data: { op: "list", prs } };
|
|
1312
|
+
}
|
|
1313
|
+
async function runReview(runtime, message, options, callback) {
|
|
1314
|
+
const selection = resolveAccountSelection(options, "user");
|
|
1315
|
+
const repo = requireString(options, "repo");
|
|
1316
|
+
const number = requireNumber(options, "number");
|
|
1317
|
+
const action = parseReviewAction(options?.action);
|
|
1318
|
+
const body = requireString(options, "body");
|
|
1319
|
+
if (!repo || !number || !action) {
|
|
1320
|
+
const err = "GITHUB_PR_OP review requires repo (owner/name), number (integer), and action (approve|request-changes|comment)";
|
|
1321
|
+
await callback?.({ text: err });
|
|
1322
|
+
return { success: false, error: err };
|
|
1323
|
+
}
|
|
1324
|
+
const parts = splitRepo(repo);
|
|
1325
|
+
if (!parts) {
|
|
1326
|
+
const err = `Invalid repo "${repo}" \u2014 expected "owner/name"`;
|
|
1327
|
+
await callback?.({ text: err });
|
|
1328
|
+
return { success: false, error: err };
|
|
1329
|
+
}
|
|
1330
|
+
const preview = `About to ${action.replace("-", " ")} PR ${repo}#${number}` + (body ? ` with body: "${body.slice(0, 120)}"` : "") + ` as ${describeSelection(selection)}.`;
|
|
1331
|
+
const decision = await requireConfirmation2({
|
|
1332
|
+
runtime,
|
|
1333
|
+
message,
|
|
1334
|
+
actionName: GitHubActions.GITHUB_PR_OP,
|
|
1335
|
+
pendingKey: `review:${repo}:${number}:${action}`,
|
|
1336
|
+
prompt: `${preview} Reply yes to confirm or no to cancel.`,
|
|
1337
|
+
callback
|
|
1338
|
+
});
|
|
1339
|
+
if (decision.status === "pending") {
|
|
1340
|
+
const text = `${preview} Reply yes to confirm or no to cancel.`;
|
|
1341
|
+
await callback?.({ text });
|
|
1342
|
+
return {
|
|
1343
|
+
success: true,
|
|
1344
|
+
text,
|
|
1345
|
+
data: { requiresConfirmation: true, preview, awaitingUserInput: true }
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
if (decision.status === "cancelled") {
|
|
1349
|
+
const text = "GitHub PR review cancelled.";
|
|
1350
|
+
await callback?.({ text });
|
|
1351
|
+
return { success: true, text, data: { cancelled: true } };
|
|
1352
|
+
}
|
|
1353
|
+
if (action === "request-changes" && !body) {
|
|
1354
|
+
const err = "request-changes review requires a body explaining the changes";
|
|
1355
|
+
await callback?.({ text: err });
|
|
1356
|
+
return { success: false, error: err };
|
|
1357
|
+
}
|
|
1358
|
+
const resolved = buildResolvedClient(runtime, selection);
|
|
1359
|
+
if ("error" in resolved) {
|
|
1360
|
+
await callback?.({ text: resolved.error });
|
|
1361
|
+
return { success: false, error: resolved.error };
|
|
1362
|
+
}
|
|
1363
|
+
const resp = await resolved.client.pulls.createReview({
|
|
1364
|
+
owner: parts.owner,
|
|
1365
|
+
repo: parts.name,
|
|
1366
|
+
pull_number: number,
|
|
1367
|
+
event: EVENT_BY_ACTION[action],
|
|
1368
|
+
body: body ?? void 0
|
|
1369
|
+
});
|
|
1370
|
+
await callback?.({ text: `Submitted ${action} review on ${repo}#${number}` });
|
|
1371
|
+
return { success: true, data: { op: "review", id: resp.data.id } };
|
|
1372
|
+
}
|
|
1373
|
+
var prOpAction = {
|
|
1374
|
+
name: GitHubActions.GITHUB_PR_OP,
|
|
1375
|
+
contexts: ["code", "tasks", "connectors", "automation"],
|
|
1376
|
+
contextGate: { anyOf: ["code", "tasks", "connectors", "automation"] },
|
|
1377
|
+
roleGate: { minRole: "USER" },
|
|
1378
|
+
similes: [
|
|
1379
|
+
"LIST_PRS",
|
|
1380
|
+
"LIST_PULL_REQUESTS",
|
|
1381
|
+
"SHOW_PRS",
|
|
1382
|
+
"GITHUB_LIST_PRS",
|
|
1383
|
+
"REVIEW_PR",
|
|
1384
|
+
"APPROVE_PR",
|
|
1385
|
+
"REQUEST_CHANGES",
|
|
1386
|
+
"COMMENT_ON_PR"
|
|
1387
|
+
],
|
|
1388
|
+
description: "Single router for GitHub PR ops: list and review. Review requires confirmed:true.",
|
|
1389
|
+
descriptionCompressed: "GitHub PR ops: list pull requests, submit review with confirmation.",
|
|
1390
|
+
parameters: [
|
|
1391
|
+
{
|
|
1392
|
+
name: "subaction",
|
|
1393
|
+
description: "PR operation: list or review.",
|
|
1394
|
+
required: true,
|
|
1395
|
+
schema: { type: "string", enum: [...SUPPORTED_OPS2] }
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
name: "repo",
|
|
1399
|
+
description: "Repository in owner/name form.",
|
|
1400
|
+
required: false,
|
|
1401
|
+
schema: { type: "string" }
|
|
1402
|
+
},
|
|
1403
|
+
{
|
|
1404
|
+
name: "number",
|
|
1405
|
+
description: "Pull request number for review.",
|
|
1406
|
+
required: false,
|
|
1407
|
+
schema: { type: "number" }
|
|
1408
|
+
},
|
|
1409
|
+
{
|
|
1410
|
+
name: "state",
|
|
1411
|
+
description: "PR state for list.",
|
|
1412
|
+
required: false,
|
|
1413
|
+
schema: {
|
|
1414
|
+
type: "string",
|
|
1415
|
+
enum: ["open", "closed", "all"],
|
|
1416
|
+
default: "open"
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
name: "author",
|
|
1421
|
+
description: "Optional PR author username filter for list.",
|
|
1422
|
+
required: false,
|
|
1423
|
+
schema: { type: "string" }
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
name: "action",
|
|
1427
|
+
description: "Review action: approve, request-changes, or comment.",
|
|
1428
|
+
required: false,
|
|
1429
|
+
schema: {
|
|
1430
|
+
type: "string",
|
|
1431
|
+
enum: ["approve", "request-changes", "comment"]
|
|
1432
|
+
}
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
name: "body",
|
|
1436
|
+
description: "Review body for comment or request-changes.",
|
|
1437
|
+
required: false,
|
|
1438
|
+
schema: { type: "string" }
|
|
1439
|
+
},
|
|
1440
|
+
{
|
|
1441
|
+
name: "as",
|
|
1442
|
+
description: "Identity to use: agent or user.",
|
|
1443
|
+
required: false,
|
|
1444
|
+
schema: { type: "string", enum: ["agent", "user"], default: "agent" }
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
name: "accountId",
|
|
1448
|
+
description: "Optional GitHub account id from GITHUB_ACCOUNTS. Defaults by role.",
|
|
1449
|
+
required: false,
|
|
1450
|
+
schema: { type: "string" }
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
name: "confirmed",
|
|
1454
|
+
description: "Must be true to submit a review.",
|
|
1455
|
+
required: false,
|
|
1456
|
+
schema: { type: "boolean", default: false }
|
|
1457
|
+
}
|
|
1458
|
+
],
|
|
1459
|
+
validate: async (runtime, _message) => {
|
|
1460
|
+
const r = buildResolvedClient(runtime, "agent");
|
|
1461
|
+
return !("error" in r);
|
|
1462
|
+
},
|
|
1463
|
+
handler: async (runtime, message, _state, options, callback) => {
|
|
1464
|
+
const op = parseOp2(options?.op);
|
|
1465
|
+
if (!op) {
|
|
1466
|
+
const err = "GITHUB_PR_OP requires op (list|review)";
|
|
1467
|
+
await callback?.({ text: err });
|
|
1468
|
+
return { success: false, error: err };
|
|
1469
|
+
}
|
|
1470
|
+
try {
|
|
1471
|
+
return op === "list" ? await runList(runtime, options, callback) : await runReview(runtime, message, options, callback);
|
|
1472
|
+
} catch (err) {
|
|
1473
|
+
const rl = inspectRateLimit(err);
|
|
1474
|
+
const message2 = rl.isRateLimited ? formatRateLimitMessage(rl) : `GITHUB_PR_OP ${op} failed: ${errorMessage(err)}`;
|
|
1475
|
+
logger3.warn({ message: message2 }, "[GitHub:GITHUB_PR_OP]");
|
|
1476
|
+
await callback?.({ text: message2 });
|
|
1477
|
+
return { success: false, error: message2 };
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
examples: [
|
|
1481
|
+
[
|
|
1482
|
+
{
|
|
1483
|
+
name: "{{user1}}",
|
|
1484
|
+
content: { text: "Show me open PRs on elizaOS/eliza" }
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
name: "{{agentName}}",
|
|
1488
|
+
content: { text: "Found 3 pull request(s)" }
|
|
1489
|
+
}
|
|
1490
|
+
],
|
|
1491
|
+
[
|
|
1492
|
+
{
|
|
1493
|
+
name: "{{user1}}",
|
|
1494
|
+
content: { text: "Approve PR #42 on elizaOS/eliza" }
|
|
1495
|
+
},
|
|
1496
|
+
{
|
|
1497
|
+
name: "{{agentName}}",
|
|
1498
|
+
content: {
|
|
1499
|
+
text: "Submitted approve review on elizaOS/eliza#42"
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
]
|
|
1503
|
+
]
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
// src/actions/github.ts
|
|
1507
|
+
var GITHUB_ACTIONS = [
|
|
1508
|
+
"pr_list",
|
|
1509
|
+
"pr_review",
|
|
1510
|
+
"issue_create",
|
|
1511
|
+
"issue_assign",
|
|
1512
|
+
"issue_close",
|
|
1513
|
+
"issue_reopen",
|
|
1514
|
+
"issue_comment",
|
|
1515
|
+
"issue_label",
|
|
1516
|
+
"notification_triage"
|
|
1517
|
+
];
|
|
1518
|
+
var ISSUE_OP_BY_ACTION = {
|
|
1519
|
+
issue_create: "create",
|
|
1520
|
+
issue_assign: "assign",
|
|
1521
|
+
issue_close: "close",
|
|
1522
|
+
issue_reopen: "reopen",
|
|
1523
|
+
issue_comment: "comment",
|
|
1524
|
+
issue_label: "label"
|
|
1525
|
+
};
|
|
1526
|
+
function readParameters(options) {
|
|
1527
|
+
if (!options || typeof options !== "object") return {};
|
|
1528
|
+
const record = options;
|
|
1529
|
+
const params = record.parameters;
|
|
1530
|
+
return params && typeof params === "object" && !Array.isArray(params) ? { ...params } : { ...record };
|
|
1531
|
+
}
|
|
1532
|
+
function readAction(options) {
|
|
1533
|
+
const params = readParameters(options);
|
|
1534
|
+
const raw = params.action ?? params.subaction ?? params.op ?? params.operation ?? params.verb;
|
|
1535
|
+
if (typeof raw !== "string") return void 0;
|
|
1536
|
+
const normalized = raw.trim().toLowerCase().replace(/[-\s]+/g, "_");
|
|
1537
|
+
return GITHUB_ACTIONS.includes(normalized) ? normalized : void 0;
|
|
1538
|
+
}
|
|
1539
|
+
function delegate(target, runtime, message, state, options, callback) {
|
|
1540
|
+
return target.handler(
|
|
1541
|
+
runtime,
|
|
1542
|
+
message,
|
|
1543
|
+
state,
|
|
1544
|
+
options,
|
|
1545
|
+
callback
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
var githubAction = {
|
|
1549
|
+
name: "GITHUB",
|
|
1550
|
+
contexts: ["code", "tasks", "connectors", "automation"],
|
|
1551
|
+
contextGate: { anyOf: ["code", "tasks", "connectors", "automation"] },
|
|
1552
|
+
roleGate: { minRole: "USER" },
|
|
1553
|
+
similes: [
|
|
1554
|
+
"GITHUB_PR_OP",
|
|
1555
|
+
"GITHUB_ISSUE_OP",
|
|
1556
|
+
"GITHUB_NOTIFICATION_TRIAGE",
|
|
1557
|
+
"GITHUB_PULL_REQUEST",
|
|
1558
|
+
"GITHUB_ISSUE",
|
|
1559
|
+
"GITHUB_NOTIFICATIONS"
|
|
1560
|
+
],
|
|
1561
|
+
description: "GitHub umbrella for pull requests, issues, and notification triage. Use action=pr_list/pr_review/issue_create/issue_assign/issue_close/issue_reopen/issue_comment/issue_label/notification_triage.",
|
|
1562
|
+
descriptionCompressed: "GitHub pr_list|pr_review|issue_create|assign|close|reopen|comment|label|triage",
|
|
1563
|
+
parameters: [
|
|
1564
|
+
{
|
|
1565
|
+
name: "action",
|
|
1566
|
+
description: "GitHub operation to run.",
|
|
1567
|
+
required: true,
|
|
1568
|
+
schema: { type: "string", enum: [...GITHUB_ACTIONS] }
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
name: "repo",
|
|
1572
|
+
description: "Repository in owner/name form.",
|
|
1573
|
+
required: false,
|
|
1574
|
+
schema: { type: "string" }
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
name: "number",
|
|
1578
|
+
description: "Pull request or issue number.",
|
|
1579
|
+
required: false,
|
|
1580
|
+
schema: { type: "number" }
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
name: "state",
|
|
1584
|
+
description: "PR state for pr_list: open, closed, or all.",
|
|
1585
|
+
required: false,
|
|
1586
|
+
schema: {
|
|
1587
|
+
type: "string",
|
|
1588
|
+
enum: ["open", "closed", "all"],
|
|
1589
|
+
default: "open"
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1592
|
+
{
|
|
1593
|
+
name: "author",
|
|
1594
|
+
description: "Optional PR author username filter for pr_list.",
|
|
1595
|
+
required: false,
|
|
1596
|
+
schema: { type: "string" }
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
name: "review_action",
|
|
1600
|
+
description: "For action=pr_review: approve, request-changes, or comment.",
|
|
1601
|
+
required: false,
|
|
1602
|
+
schema: {
|
|
1603
|
+
type: "string",
|
|
1604
|
+
enum: ["approve", "request-changes", "comment"]
|
|
1605
|
+
}
|
|
1606
|
+
},
|
|
1607
|
+
{
|
|
1608
|
+
name: "title",
|
|
1609
|
+
description: "Issue title for action=issue_create.",
|
|
1610
|
+
required: false,
|
|
1611
|
+
schema: { type: "string" }
|
|
1612
|
+
},
|
|
1613
|
+
{
|
|
1614
|
+
name: "body",
|
|
1615
|
+
description: "Issue body, issue comment body, or PR review body.",
|
|
1616
|
+
required: false,
|
|
1617
|
+
schema: { type: "string" }
|
|
1618
|
+
},
|
|
1619
|
+
{
|
|
1620
|
+
name: "assignees",
|
|
1621
|
+
description: "GitHub usernames to assign.",
|
|
1622
|
+
required: false,
|
|
1623
|
+
schema: { type: "array", items: { type: "string" } }
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "labels",
|
|
1627
|
+
description: "Labels to apply on issue create or issue_label.",
|
|
1628
|
+
required: false,
|
|
1629
|
+
schema: { type: "array", items: { type: "string" } }
|
|
1630
|
+
},
|
|
1631
|
+
{
|
|
1632
|
+
name: "as",
|
|
1633
|
+
description: "Identity to use: agent or user.",
|
|
1634
|
+
required: false,
|
|
1635
|
+
schema: { type: "string", enum: ["agent", "user"], default: "agent" }
|
|
1636
|
+
},
|
|
1637
|
+
{
|
|
1638
|
+
name: "accountId",
|
|
1639
|
+
description: "Optional GitHub account id from GITHUB_ACCOUNTS. Defaults by role.",
|
|
1640
|
+
required: false,
|
|
1641
|
+
schema: { type: "string" }
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
name: "confirmed",
|
|
1645
|
+
description: "Must be true for GitHub write operations.",
|
|
1646
|
+
required: false,
|
|
1647
|
+
schema: { type: "boolean", default: false }
|
|
1648
|
+
}
|
|
1649
|
+
],
|
|
1650
|
+
validate: async (runtime, message, state) => await prOpAction.validate(runtime, message, state) || await issueOpAction.validate(runtime, message, state) || await notificationTriageAction.validate(runtime, message, state),
|
|
1651
|
+
handler: async (runtime, message, state, options, callback) => {
|
|
1652
|
+
const action = readAction(options);
|
|
1653
|
+
const params = readParameters(options);
|
|
1654
|
+
if (!action) {
|
|
1655
|
+
return {
|
|
1656
|
+
success: false,
|
|
1657
|
+
text: "GITHUB requires action=pr_list/pr_review/issue_create/issue_assign/issue_close/issue_reopen/issue_comment/issue_label/notification_triage.",
|
|
1658
|
+
data: { error: "MISSING_ACTION" }
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
if (action === "notification_triage") {
|
|
1662
|
+
return delegate(
|
|
1663
|
+
notificationTriageAction,
|
|
1664
|
+
runtime,
|
|
1665
|
+
message,
|
|
1666
|
+
state,
|
|
1667
|
+
params,
|
|
1668
|
+
callback
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
if (action === "pr_list" || action === "pr_review") {
|
|
1672
|
+
const childParams = {
|
|
1673
|
+
...params,
|
|
1674
|
+
op: action === "pr_list" ? "list" : "review",
|
|
1675
|
+
...action === "pr_review" && params.review_action ? { action: params.review_action } : {}
|
|
1676
|
+
};
|
|
1677
|
+
return delegate(
|
|
1678
|
+
prOpAction,
|
|
1679
|
+
runtime,
|
|
1680
|
+
message,
|
|
1681
|
+
state,
|
|
1682
|
+
childParams,
|
|
1683
|
+
callback
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
const issueOp = ISSUE_OP_BY_ACTION[action];
|
|
1687
|
+
if (issueOp) {
|
|
1688
|
+
return delegate(
|
|
1689
|
+
issueOpAction,
|
|
1690
|
+
runtime,
|
|
1691
|
+
message,
|
|
1692
|
+
state,
|
|
1693
|
+
{ ...params, op: issueOp },
|
|
1694
|
+
callback
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
success: false,
|
|
1699
|
+
text: `Unsupported GITHUB action: ${action}`,
|
|
1700
|
+
data: { error: "UNSUPPORTED_ACTION", action }
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// src/connector-account-provider.ts
|
|
1706
|
+
import {
|
|
1707
|
+
logger as logger4
|
|
1708
|
+
} from "@elizaos/core";
|
|
1709
|
+
var GITHUB_AUTHORIZATION_ENDPOINT = "https://github.com/login/oauth/authorize";
|
|
1710
|
+
var GITHUB_TOKEN_ENDPOINT = "https://github.com/login/oauth/access_token";
|
|
1711
|
+
var GITHUB_USER_ENDPOINT = "https://api.github.com/user";
|
|
1712
|
+
var DEFAULT_PURPOSES = [
|
|
1713
|
+
"posting",
|
|
1714
|
+
"reading",
|
|
1715
|
+
"admin"
|
|
1716
|
+
];
|
|
1717
|
+
function nonEmptyString3(value) {
|
|
1718
|
+
if (typeof value !== "string") return void 0;
|
|
1719
|
+
const trimmed = value.trim();
|
|
1720
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
1721
|
+
}
|
|
1722
|
+
function readSetting2(runtime, key) {
|
|
1723
|
+
return nonEmptyString3(runtime.getSetting?.(key));
|
|
1724
|
+
}
|
|
1725
|
+
function readClientConfig(runtime) {
|
|
1726
|
+
const clientId = readSetting2(runtime, "GITHUB_OAUTH_CLIENT_ID");
|
|
1727
|
+
const clientSecret = readSetting2(runtime, "GITHUB_OAUTH_CLIENT_SECRET");
|
|
1728
|
+
const redirectUri = readSetting2(runtime, "GITHUB_OAUTH_REDIRECT_URI");
|
|
1729
|
+
if (!clientId || !clientSecret || !redirectUri) {
|
|
1730
|
+
throw new Error(
|
|
1731
|
+
"GitHub OAuth requires GITHUB_OAUTH_CLIENT_ID, GITHUB_OAUTH_CLIENT_SECRET, and GITHUB_OAUTH_REDIRECT_URI to be configured."
|
|
1732
|
+
);
|
|
1733
|
+
}
|
|
1734
|
+
return { clientId, clientSecret, redirectUri };
|
|
1735
|
+
}
|
|
1736
|
+
function parseScopes(value) {
|
|
1737
|
+
if (!value) return [];
|
|
1738
|
+
return value.split(/[,\s]+/).map((scope) => scope.trim()).filter(Boolean);
|
|
1739
|
+
}
|
|
1740
|
+
function defaultRoleFromAccountId(accountId) {
|
|
1741
|
+
if (accountId === DEFAULT_GITHUB_USER_ACCOUNT_ID) return "OWNER";
|
|
1742
|
+
if (accountId === DEFAULT_GITHUB_AGENT_ACCOUNT_ID) return "AGENT";
|
|
1743
|
+
return "OWNER";
|
|
1744
|
+
}
|
|
1745
|
+
function roleFromMetadata(metadata, accountId) {
|
|
1746
|
+
const record = metadata && typeof metadata === "object" && !Array.isArray(metadata) ? metadata : {};
|
|
1747
|
+
const raw = nonEmptyString3(record.role ?? record.accountRole);
|
|
1748
|
+
const normalized = raw?.toUpperCase();
|
|
1749
|
+
if (normalized === "OWNER" || normalized === "AGENT" || normalized === "TEAM") {
|
|
1750
|
+
return normalized;
|
|
1751
|
+
}
|
|
1752
|
+
return defaultRoleFromAccountId(accountId);
|
|
1753
|
+
}
|
|
1754
|
+
async function exchangeCodeForToken(args) {
|
|
1755
|
+
const response = await fetch(GITHUB_TOKEN_ENDPOINT, {
|
|
1756
|
+
method: "POST",
|
|
1757
|
+
headers: {
|
|
1758
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1759
|
+
Accept: "application/json"
|
|
1760
|
+
},
|
|
1761
|
+
body: new URLSearchParams({
|
|
1762
|
+
client_id: args.clientId,
|
|
1763
|
+
client_secret: args.clientSecret,
|
|
1764
|
+
code: args.code,
|
|
1765
|
+
redirect_uri: args.redirectUri
|
|
1766
|
+
}).toString()
|
|
1767
|
+
});
|
|
1768
|
+
if (!response.ok) {
|
|
1769
|
+
const body = await response.text();
|
|
1770
|
+
throw new Error(
|
|
1771
|
+
`GitHub token exchange failed with ${response.status}: ${body}`
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
const parsed = await response.json();
|
|
1775
|
+
if (parsed.error) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`GitHub token exchange returned error ${parsed.error}: ${parsed.error_description ?? "no description"}`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
if (!parsed.access_token) {
|
|
1781
|
+
throw new Error("GitHub token exchange returned no access_token.");
|
|
1782
|
+
}
|
|
1783
|
+
return parsed;
|
|
1784
|
+
}
|
|
1785
|
+
async function fetchGitHubUser(accessToken) {
|
|
1786
|
+
const response = await fetch(GITHUB_USER_ENDPOINT, {
|
|
1787
|
+
headers: {
|
|
1788
|
+
Authorization: `Bearer ${accessToken}`,
|
|
1789
|
+
Accept: "application/vnd.github+json",
|
|
1790
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
if (!response.ok) {
|
|
1794
|
+
throw new Error(`GitHub /user request failed with ${response.status}`);
|
|
1795
|
+
}
|
|
1796
|
+
const parsed = await response.json();
|
|
1797
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1798
|
+
throw new Error("GitHub /user returned an invalid payload.");
|
|
1799
|
+
}
|
|
1800
|
+
return parsed;
|
|
1801
|
+
}
|
|
1802
|
+
function synthesizeEnvAccounts(runtime) {
|
|
1803
|
+
const now = Date.now();
|
|
1804
|
+
return readGitHubAccounts(runtime).map((account) => ({
|
|
1805
|
+
id: account.accountId,
|
|
1806
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1807
|
+
label: account.label ?? `GitHub ${account.role} (${account.accountId})`,
|
|
1808
|
+
role: account.role === "user" ? "OWNER" : "AGENT",
|
|
1809
|
+
purpose: DEFAULT_PURPOSES,
|
|
1810
|
+
accessGate: "open",
|
|
1811
|
+
status: "connected",
|
|
1812
|
+
displayHandle: account.accountId,
|
|
1813
|
+
createdAt: now,
|
|
1814
|
+
updatedAt: now,
|
|
1815
|
+
metadata: { authMethod: "pat", source: "env" }
|
|
1816
|
+
}));
|
|
1817
|
+
}
|
|
1818
|
+
function createGitHubConnectorAccountProvider(runtime) {
|
|
1819
|
+
return {
|
|
1820
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1821
|
+
label: "GitHub",
|
|
1822
|
+
listAccounts: async (manager) => {
|
|
1823
|
+
const stored = await manager.getStorage().listAccounts(GITHUB_SERVICE_TYPE);
|
|
1824
|
+
if (stored.length > 0) return stored;
|
|
1825
|
+
return synthesizeEnvAccounts(runtime);
|
|
1826
|
+
},
|
|
1827
|
+
createAccount: async (input, _manager) => {
|
|
1828
|
+
return {
|
|
1829
|
+
...input,
|
|
1830
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1831
|
+
role: input.role ?? "OWNER",
|
|
1832
|
+
purpose: input.purpose ?? DEFAULT_PURPOSES,
|
|
1833
|
+
accessGate: input.accessGate ?? "open",
|
|
1834
|
+
status: input.status ?? "pending"
|
|
1835
|
+
};
|
|
1836
|
+
},
|
|
1837
|
+
patchAccount: async (_accountId, patch, _manager) => {
|
|
1838
|
+
return { ...patch, provider: GITHUB_SERVICE_TYPE };
|
|
1839
|
+
},
|
|
1840
|
+
deleteAccount: async (_accountId, _manager) => {
|
|
1841
|
+
},
|
|
1842
|
+
startOAuth: async (request, _manager) => {
|
|
1843
|
+
const config = readClientConfig(runtime);
|
|
1844
|
+
const redirectUri = request.redirectUri ?? config.redirectUri;
|
|
1845
|
+
const scopes = request.scopes && request.scopes.length > 0 ? request.scopes : ["repo", "read:user", "user:email", "notifications"];
|
|
1846
|
+
const params = new URLSearchParams({
|
|
1847
|
+
client_id: config.clientId,
|
|
1848
|
+
redirect_uri: redirectUri,
|
|
1849
|
+
state: request.flow.state,
|
|
1850
|
+
scope: scopes.join(" "),
|
|
1851
|
+
allow_signup: "false"
|
|
1852
|
+
});
|
|
1853
|
+
return {
|
|
1854
|
+
authUrl: `${GITHUB_AUTHORIZATION_ENDPOINT}?${params.toString()}`,
|
|
1855
|
+
metadata: {
|
|
1856
|
+
...request.metadata,
|
|
1857
|
+
requestedScopes: scopes,
|
|
1858
|
+
redirectUri
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
},
|
|
1862
|
+
completeOAuth: async (request, manager) => {
|
|
1863
|
+
const code = nonEmptyString3(request.code);
|
|
1864
|
+
if (!code) {
|
|
1865
|
+
throw new Error(
|
|
1866
|
+
"GitHub OAuth callback is missing an authorization code."
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
const config = readClientConfig(runtime);
|
|
1870
|
+
const redirectUri = nonEmptyString3(request.flow.redirectUri) ?? config.redirectUri;
|
|
1871
|
+
const tokens = await exchangeCodeForToken({
|
|
1872
|
+
clientId: config.clientId,
|
|
1873
|
+
clientSecret: config.clientSecret,
|
|
1874
|
+
redirectUri,
|
|
1875
|
+
code
|
|
1876
|
+
});
|
|
1877
|
+
if (!tokens.access_token) {
|
|
1878
|
+
throw new Error("GitHub token exchange returned no access_token.");
|
|
1879
|
+
}
|
|
1880
|
+
const user = await fetchGitHubUser(tokens.access_token);
|
|
1881
|
+
const externalId = nonEmptyString3(user.id ? String(user.id) : void 0);
|
|
1882
|
+
const login = nonEmptyString3(user.login);
|
|
1883
|
+
if (!login) {
|
|
1884
|
+
throw new Error("GitHub /user payload did not include a login.");
|
|
1885
|
+
}
|
|
1886
|
+
const expiresAt = typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0;
|
|
1887
|
+
const oauthCredentialVersion = String(Date.now());
|
|
1888
|
+
const accountMetadata = {
|
|
1889
|
+
authMethod: "oauth",
|
|
1890
|
+
login,
|
|
1891
|
+
githubUserId: user.id ?? null,
|
|
1892
|
+
email: nonEmptyString3(user.email) ?? null,
|
|
1893
|
+
type: nonEmptyString3(user.type) ?? null,
|
|
1894
|
+
tokenType: nonEmptyString3(tokens.token_type) ?? "bearer",
|
|
1895
|
+
grantedScopes: parseScopes(tokens.scope),
|
|
1896
|
+
hasRefreshToken: Boolean(tokens.refresh_token),
|
|
1897
|
+
expiresAt,
|
|
1898
|
+
oauthCredentialVersion
|
|
1899
|
+
};
|
|
1900
|
+
const pendingAccount = await manager.upsertAccount(
|
|
1901
|
+
GITHUB_SERVICE_TYPE,
|
|
1902
|
+
{
|
|
1903
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1904
|
+
role: roleFromMetadata(request.flow.metadata, request.flow.accountId),
|
|
1905
|
+
purpose: DEFAULT_PURPOSES,
|
|
1906
|
+
accessGate: "open",
|
|
1907
|
+
status: "pending",
|
|
1908
|
+
externalId: externalId ?? login,
|
|
1909
|
+
displayHandle: login,
|
|
1910
|
+
label: nonEmptyString3(user.name) ?? login,
|
|
1911
|
+
metadata: accountMetadata
|
|
1912
|
+
},
|
|
1913
|
+
request.flow.accountId
|
|
1914
|
+
);
|
|
1915
|
+
const credentialPersist = await persistConnectorCredentialRefs({
|
|
1916
|
+
runtime,
|
|
1917
|
+
manager,
|
|
1918
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1919
|
+
accountIdForRef: pendingAccount.id,
|
|
1920
|
+
storageAccountId: pendingAccount.id,
|
|
1921
|
+
caller: "plugin-github",
|
|
1922
|
+
credentials: [
|
|
1923
|
+
{
|
|
1924
|
+
credentialType: "oauth.tokens",
|
|
1925
|
+
value: JSON.stringify({
|
|
1926
|
+
access_token: tokens.access_token,
|
|
1927
|
+
...tokens.refresh_token ? { refresh_token: tokens.refresh_token } : {},
|
|
1928
|
+
...expiresAt !== void 0 ? { expires_at: expiresAt } : {},
|
|
1929
|
+
token_type: nonEmptyString3(tokens.token_type) ?? "bearer",
|
|
1930
|
+
scope: tokens.scope ?? ""
|
|
1931
|
+
}),
|
|
1932
|
+
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
1933
|
+
metadata: {
|
|
1934
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1935
|
+
hasRefreshToken: Boolean(tokens.refresh_token)
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
]
|
|
1939
|
+
});
|
|
1940
|
+
const accountPatch = {
|
|
1941
|
+
...pendingAccount,
|
|
1942
|
+
id: pendingAccount.id,
|
|
1943
|
+
provider: GITHUB_SERVICE_TYPE,
|
|
1944
|
+
status: "connected",
|
|
1945
|
+
metadata: {
|
|
1946
|
+
...accountMetadata,
|
|
1947
|
+
credentialRefs: credentialPersist.refs,
|
|
1948
|
+
credentialRefStorage: {
|
|
1949
|
+
vaultAvailable: credentialPersist.vaultAvailable,
|
|
1950
|
+
storageAvailable: credentialPersist.storageAvailable
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
logger4.info(
|
|
1955
|
+
{
|
|
1956
|
+
src: "plugin:github:connector",
|
|
1957
|
+
login
|
|
1958
|
+
},
|
|
1959
|
+
"GitHub OAuth completed"
|
|
1960
|
+
);
|
|
1961
|
+
return {
|
|
1962
|
+
account: accountPatch,
|
|
1963
|
+
flow: { status: "completed" }
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// src/routes/github-routes.ts
|
|
1970
|
+
import { logger as logger5 } from "@elizaos/core";
|
|
1971
|
+
|
|
1972
|
+
// src/github-credentials.ts
|
|
1973
|
+
import fs from "fs/promises";
|
|
1974
|
+
import path from "path";
|
|
1975
|
+
import { resolveStateDir } from "@elizaos/core";
|
|
1976
|
+
function getCredentialFilePath() {
|
|
1977
|
+
return path.join(resolveStateDir(), "credentials", "github.json");
|
|
1978
|
+
}
|
|
1979
|
+
function isGitHubCredentials(value) {
|
|
1980
|
+
if (!value || typeof value !== "object") return false;
|
|
1981
|
+
const v = value;
|
|
1982
|
+
return typeof v.token === "string" && typeof v.username === "string" && Array.isArray(v.scopes) && v.scopes.every((s) => typeof s === "string") && typeof v.savedAt === "number";
|
|
1983
|
+
}
|
|
1984
|
+
async function loadCredentials() {
|
|
1985
|
+
const filePath = getCredentialFilePath();
|
|
1986
|
+
let raw;
|
|
1987
|
+
try {
|
|
1988
|
+
raw = await fs.readFile(filePath, "utf-8");
|
|
1989
|
+
} catch {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
let parsed;
|
|
1993
|
+
try {
|
|
1994
|
+
parsed = JSON.parse(raw);
|
|
1995
|
+
} catch {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
return isGitHubCredentials(parsed) ? parsed : null;
|
|
1999
|
+
}
|
|
2000
|
+
async function loadMetadata() {
|
|
2001
|
+
const creds = await loadCredentials();
|
|
2002
|
+
if (!creds) return null;
|
|
2003
|
+
const { token: _token, ...metadata } = creds;
|
|
2004
|
+
return metadata;
|
|
2005
|
+
}
|
|
2006
|
+
async function saveCredentials(creds) {
|
|
2007
|
+
const filePath = getCredentialFilePath();
|
|
2008
|
+
const directory = path.dirname(filePath);
|
|
2009
|
+
await fs.mkdir(directory, { recursive: true, mode: 448 });
|
|
2010
|
+
await fs.chmod(directory, 448);
|
|
2011
|
+
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
2012
|
+
await fs.writeFile(tmpPath, JSON.stringify(creds, null, 2), {
|
|
2013
|
+
mode: 384
|
|
2014
|
+
});
|
|
2015
|
+
await fs.rename(tmpPath, filePath);
|
|
2016
|
+
}
|
|
2017
|
+
async function clearCredentials() {
|
|
2018
|
+
const filePath = getCredentialFilePath();
|
|
2019
|
+
try {
|
|
2020
|
+
await fs.unlink(filePath);
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
if (err.code === "ENOENT") return;
|
|
2023
|
+
throw err;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
function buildCredentialsFromUserResponse(token, user, scopes, now = Date.now()) {
|
|
2027
|
+
return {
|
|
2028
|
+
token,
|
|
2029
|
+
username: user.login,
|
|
2030
|
+
scopes,
|
|
2031
|
+
savedAt: now
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// src/routes/github-routes.ts
|
|
2036
|
+
var GITHUB_USER_URL = "https://api.github.com/user";
|
|
2037
|
+
var VALIDATION_TIMEOUT_MS = 1e4;
|
|
2038
|
+
var MAX_BODY_BYTES = 8 * 1024;
|
|
2039
|
+
async function readJsonBody(req) {
|
|
2040
|
+
const chunks = [];
|
|
2041
|
+
let total = 0;
|
|
2042
|
+
for await (const chunk of req) {
|
|
2043
|
+
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
2044
|
+
total += buf.length;
|
|
2045
|
+
if (total > MAX_BODY_BYTES) return null;
|
|
2046
|
+
chunks.push(buf);
|
|
2047
|
+
}
|
|
2048
|
+
if (chunks.length === 0) return null;
|
|
2049
|
+
try {
|
|
2050
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
2051
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
2052
|
+
} catch {
|
|
2053
|
+
return null;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
function sendJson(ctx, status, body) {
|
|
2057
|
+
if (ctx.json) {
|
|
2058
|
+
ctx.json(status, body);
|
|
2059
|
+
return;
|
|
2060
|
+
}
|
|
2061
|
+
ctx.res.statusCode = status;
|
|
2062
|
+
ctx.res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
2063
|
+
ctx.res.end(JSON.stringify(body));
|
|
2064
|
+
}
|
|
2065
|
+
function metadataToStatus(metadata) {
|
|
2066
|
+
if (!metadata) return { connected: false };
|
|
2067
|
+
return {
|
|
2068
|
+
connected: true,
|
|
2069
|
+
username: metadata.username,
|
|
2070
|
+
scopes: metadata.scopes,
|
|
2071
|
+
savedAt: metadata.savedAt
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
async function validateToken(token, fetchImpl) {
|
|
2075
|
+
const controller = new AbortController();
|
|
2076
|
+
const timer = setTimeout(() => controller.abort(), VALIDATION_TIMEOUT_MS);
|
|
2077
|
+
let response;
|
|
2078
|
+
try {
|
|
2079
|
+
response = await fetchImpl(GITHUB_USER_URL, {
|
|
2080
|
+
headers: {
|
|
2081
|
+
Authorization: `Bearer ${token}`,
|
|
2082
|
+
Accept: "application/vnd.github+json",
|
|
2083
|
+
"User-Agent": "eliza-github-connection"
|
|
2084
|
+
},
|
|
2085
|
+
signal: controller.signal
|
|
2086
|
+
});
|
|
2087
|
+
} finally {
|
|
2088
|
+
clearTimeout(timer);
|
|
2089
|
+
}
|
|
2090
|
+
if (response.status === 401) {
|
|
2091
|
+
throw new Error("Token rejected by GitHub: bad credentials.");
|
|
2092
|
+
}
|
|
2093
|
+
if (response.status === 403) {
|
|
2094
|
+
throw new Error(
|
|
2095
|
+
"Token rejected by GitHub: forbidden. Check the token has at least `read:user` scope."
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
if (!response.ok) {
|
|
2099
|
+
throw new Error(
|
|
2100
|
+
`GitHub returned ${response.status} validating the token. Try again or generate a new token.`
|
|
2101
|
+
);
|
|
2102
|
+
}
|
|
2103
|
+
const body = await response.json();
|
|
2104
|
+
if (typeof body?.login !== "string" || body.login.length === 0) {
|
|
2105
|
+
throw new Error("GitHub /user response was missing the login field.");
|
|
2106
|
+
}
|
|
2107
|
+
const scopesHeader = response.headers.get("x-oauth-scopes") ?? "";
|
|
2108
|
+
const scopes = scopesHeader.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
2109
|
+
return { user: body, scopes };
|
|
2110
|
+
}
|
|
2111
|
+
async function handleGetToken(ctx) {
|
|
2112
|
+
const metadata = await loadMetadata();
|
|
2113
|
+
sendJson(ctx, 200, metadataToStatus(metadata));
|
|
2114
|
+
return true;
|
|
2115
|
+
}
|
|
2116
|
+
async function handlePostToken(ctx) {
|
|
2117
|
+
const body = await readJsonBody(ctx.req);
|
|
2118
|
+
const token = body && typeof body.token === "string" ? body.token.trim() : "";
|
|
2119
|
+
if (token.length === 0) {
|
|
2120
|
+
sendJson(ctx, 400, { error: "Missing `token` in request body." });
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
const fetchImpl = ctx.fetch ?? fetch;
|
|
2124
|
+
let validated;
|
|
2125
|
+
try {
|
|
2126
|
+
validated = await validateToken(token, fetchImpl);
|
|
2127
|
+
} catch (err) {
|
|
2128
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2129
|
+
logger5.warn(`[github-routes] token validation failed: ${message}`);
|
|
2130
|
+
sendJson(ctx, 400, { error: message });
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
const credentials = buildCredentialsFromUserResponse(
|
|
2134
|
+
token,
|
|
2135
|
+
validated.user,
|
|
2136
|
+
validated.scopes
|
|
2137
|
+
);
|
|
2138
|
+
await saveCredentials(credentials);
|
|
2139
|
+
logger5.info(
|
|
2140
|
+
`[github-routes] saved github token for @${validated.user.login} (scopes=${validated.scopes.join(",") || "(none)"})`
|
|
2141
|
+
);
|
|
2142
|
+
sendJson(ctx, 200, metadataToStatus(credentials));
|
|
2143
|
+
return true;
|
|
2144
|
+
}
|
|
2145
|
+
async function handleDeleteToken(ctx) {
|
|
2146
|
+
await clearCredentials();
|
|
2147
|
+
logger5.info("[github-routes] cleared saved github token");
|
|
2148
|
+
sendJson(ctx, 200, { connected: false });
|
|
2149
|
+
return true;
|
|
2150
|
+
}
|
|
2151
|
+
async function handleGitHubRoutes(ctx) {
|
|
2152
|
+
if (ctx.pathname !== "/api/github/token") return false;
|
|
2153
|
+
switch (ctx.method) {
|
|
2154
|
+
case "GET":
|
|
2155
|
+
return handleGetToken(ctx);
|
|
2156
|
+
case "POST":
|
|
2157
|
+
return handlePostToken(ctx);
|
|
2158
|
+
case "DELETE":
|
|
2159
|
+
return handleDeleteToken(ctx);
|
|
2160
|
+
default:
|
|
2161
|
+
sendJson(ctx, 405, { error: "Method not allowed" });
|
|
2162
|
+
return true;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// src/search-category.ts
|
|
2167
|
+
var GITHUB_PULL_REQUESTS_SEARCH_CATEGORY = {
|
|
2168
|
+
category: "github_pull_requests",
|
|
2169
|
+
label: "GitHub pull requests",
|
|
2170
|
+
description: "Search GitHub pull requests in a repo or across accessible repositories.",
|
|
2171
|
+
contexts: ["code", "automation"],
|
|
2172
|
+
filters: [
|
|
2173
|
+
{ name: "query", label: "Query", type: "string" },
|
|
2174
|
+
{
|
|
2175
|
+
name: "repo",
|
|
2176
|
+
label: "Repository",
|
|
2177
|
+
description: "Repository in owner/name format. Omit to search accessible repositories.",
|
|
2178
|
+
type: "string"
|
|
2179
|
+
},
|
|
2180
|
+
{
|
|
2181
|
+
name: "state",
|
|
2182
|
+
label: "State",
|
|
2183
|
+
description: "Pull request state.",
|
|
2184
|
+
type: "enum",
|
|
2185
|
+
default: "open",
|
|
2186
|
+
options: [
|
|
2187
|
+
{ label: "Open", value: "open" },
|
|
2188
|
+
{ label: "Closed", value: "closed" },
|
|
2189
|
+
{ label: "All", value: "all" }
|
|
2190
|
+
]
|
|
2191
|
+
},
|
|
2192
|
+
{
|
|
2193
|
+
name: "author",
|
|
2194
|
+
label: "Author",
|
|
2195
|
+
description: "GitHub login to filter by.",
|
|
2196
|
+
type: "string"
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
name: "as",
|
|
2200
|
+
label: "Identity",
|
|
2201
|
+
description: "Configured GitHub identity token to use.",
|
|
2202
|
+
type: "enum",
|
|
2203
|
+
default: "agent",
|
|
2204
|
+
options: [
|
|
2205
|
+
{ label: "Agent", value: "agent" },
|
|
2206
|
+
{ label: "User", value: "user" }
|
|
2207
|
+
]
|
|
2208
|
+
},
|
|
2209
|
+
{
|
|
2210
|
+
name: "accountId",
|
|
2211
|
+
label: "Account",
|
|
2212
|
+
description: "Optional configured GitHub account id. Defaults by identity role.",
|
|
2213
|
+
type: "string"
|
|
2214
|
+
},
|
|
2215
|
+
{
|
|
2216
|
+
name: "limit",
|
|
2217
|
+
label: "Limit",
|
|
2218
|
+
description: "Maximum pull requests to return.",
|
|
2219
|
+
type: "number",
|
|
2220
|
+
default: 50
|
|
2221
|
+
}
|
|
2222
|
+
],
|
|
2223
|
+
resultSchemaSummary: "PRSummary[] with repo, number, title, author, state, and url.",
|
|
2224
|
+
capabilities: ["pull-requests", "issues-search", "repositories"],
|
|
2225
|
+
source: "plugin:github",
|
|
2226
|
+
serviceType: "github"
|
|
2227
|
+
};
|
|
2228
|
+
function hasSearchCategory(runtime, category) {
|
|
2229
|
+
try {
|
|
2230
|
+
runtime.getSearchCategory(category, { includeDisabled: true });
|
|
2231
|
+
return true;
|
|
2232
|
+
} catch {
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
function registerGitHubSearchCategory(runtime) {
|
|
2237
|
+
if (!hasSearchCategory(runtime, GITHUB_PULL_REQUESTS_SEARCH_CATEGORY.category)) {
|
|
2238
|
+
runtime.registerSearchCategory(GITHUB_PULL_REQUESTS_SEARCH_CATEGORY);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/services/github-service.ts
|
|
2243
|
+
import { logger as logger6, Service } from "@elizaos/core";
|
|
2244
|
+
import { Octokit } from "@octokit/rest";
|
|
2245
|
+
function normalizeSelector(selector) {
|
|
2246
|
+
if (selector === "user" || selector === "agent") {
|
|
2247
|
+
return { role: selector };
|
|
2248
|
+
}
|
|
2249
|
+
return {
|
|
2250
|
+
accountId: typeof selector.accountId === "string" && selector.accountId.trim() ? selector.accountId.trim() : void 0,
|
|
2251
|
+
role: selector.role ?? selector.as ?? "agent"
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
var GitHubService = class _GitHubService extends Service {
|
|
2255
|
+
constructor(runtime, createClient = (auth) => new Octokit({ auth })) {
|
|
2256
|
+
super(runtime);
|
|
2257
|
+
this.createClient = createClient;
|
|
2258
|
+
}
|
|
2259
|
+
createClient;
|
|
2260
|
+
static serviceType = GITHUB_SERVICE_TYPE;
|
|
2261
|
+
capabilityDescription = "GitHub REST API integration for PRs, issues, and notifications";
|
|
2262
|
+
clients = /* @__PURE__ */ new Map();
|
|
2263
|
+
static async start(runtime, createClient) {
|
|
2264
|
+
const service = new _GitHubService(runtime, createClient);
|
|
2265
|
+
await service.initialize();
|
|
2266
|
+
return service;
|
|
2267
|
+
}
|
|
2268
|
+
async initialize() {
|
|
2269
|
+
if (!this.runtime) {
|
|
2270
|
+
return;
|
|
2271
|
+
}
|
|
2272
|
+
const accounts = await readGitHubAccountsWithConnectorCredentials(
|
|
2273
|
+
this.runtime
|
|
2274
|
+
);
|
|
2275
|
+
this.clients.clear();
|
|
2276
|
+
for (const account of accounts) {
|
|
2277
|
+
this.clients.set(account.accountId, {
|
|
2278
|
+
account,
|
|
2279
|
+
client: this.createClient(account.token)
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
for (const role of ["user", "agent"]) {
|
|
2283
|
+
if (!resolveGitHubAccount(accounts, { role })) {
|
|
2284
|
+
logger6.info(
|
|
2285
|
+
`[GitHubService] no GitHub ${role} account configured \u2014 ${role}-acting calls will be rejected`
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
logger6.info(
|
|
2290
|
+
`[GitHubService] configured ${this.clients.size} GitHub account(s)`
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
getOctokit(selector) {
|
|
2294
|
+
const selection = normalizeSelector(selector);
|
|
2295
|
+
const account = resolveGitHubAccount(
|
|
2296
|
+
Array.from(this.clients.values()).map((record) => record.account),
|
|
2297
|
+
selection
|
|
2298
|
+
);
|
|
2299
|
+
if (!account) {
|
|
2300
|
+
return null;
|
|
2301
|
+
}
|
|
2302
|
+
return this.clients.get(account.accountId)?.client ?? null;
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Allows tests to inject an Octokit-shaped mock without going through
|
|
2306
|
+
* environment variables. Not part of the public runtime contract.
|
|
2307
|
+
*/
|
|
2308
|
+
setClientForTesting(as, client, accountId = defaultGitHubAccountIdForRole(as)) {
|
|
2309
|
+
if (!client) {
|
|
2310
|
+
this.clients.delete(accountId);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
this.clients.set(accountId, {
|
|
2314
|
+
account: { accountId, role: as, token: "test" },
|
|
2315
|
+
client
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
async stop() {
|
|
2319
|
+
this.clients.clear();
|
|
2320
|
+
}
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
// src/index.ts
|
|
2324
|
+
function createGitHubRouteHandler(method) {
|
|
2325
|
+
return async (req, res, runtime) => {
|
|
2326
|
+
const httpReq = req;
|
|
2327
|
+
const httpRes = res;
|
|
2328
|
+
const url = new URL(httpReq.url ?? "/api/github/token", "http://localhost");
|
|
2329
|
+
void runtime;
|
|
2330
|
+
await handleGitHubRoutes({
|
|
2331
|
+
req: httpReq,
|
|
2332
|
+
res: httpRes,
|
|
2333
|
+
method,
|
|
2334
|
+
pathname: url.pathname
|
|
2335
|
+
});
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
var githubRoutes = [
|
|
2339
|
+
{
|
|
2340
|
+
type: "GET",
|
|
2341
|
+
path: "/api/github/token",
|
|
2342
|
+
rawPath: true,
|
|
2343
|
+
handler: createGitHubRouteHandler("GET")
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
type: "POST",
|
|
2347
|
+
path: "/api/github/token",
|
|
2348
|
+
rawPath: true,
|
|
2349
|
+
handler: createGitHubRouteHandler("POST")
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
type: "DELETE",
|
|
2353
|
+
path: "/api/github/token",
|
|
2354
|
+
rawPath: true,
|
|
2355
|
+
handler: createGitHubRouteHandler("DELETE")
|
|
2356
|
+
}
|
|
2357
|
+
];
|
|
2358
|
+
var githubPlugin = {
|
|
2359
|
+
name: "github",
|
|
2360
|
+
description: "GitHub integration for pull requests, issues, and notification triage",
|
|
2361
|
+
services: [GitHubService],
|
|
2362
|
+
actions: [...promoteSubactionsToActions(githubAction)],
|
|
2363
|
+
routes: githubRoutes,
|
|
2364
|
+
init: async (_config, runtime) => {
|
|
2365
|
+
registerGitHubSearchCategory(runtime);
|
|
2366
|
+
try {
|
|
2367
|
+
const manager = getConnectorAccountManager2(runtime);
|
|
2368
|
+
manager.registerProvider(createGitHubConnectorAccountProvider(runtime));
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
logger7.warn(
|
|
2371
|
+
{
|
|
2372
|
+
src: "plugin:github",
|
|
2373
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2374
|
+
},
|
|
2375
|
+
"Failed to register GitHub provider with ConnectorAccountManager"
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
},
|
|
2379
|
+
async dispose(runtime) {
|
|
2380
|
+
const svc = runtime.getService(GitHubService.serviceType);
|
|
2381
|
+
await svc?.stop();
|
|
2382
|
+
}
|
|
2383
|
+
};
|
|
2384
|
+
var index_default = githubPlugin;
|
|
2385
|
+
export {
|
|
2386
|
+
DEFAULT_GITHUB_AGENT_ACCOUNT_ID,
|
|
2387
|
+
DEFAULT_GITHUB_USER_ACCOUNT_ID,
|
|
2388
|
+
GITHUB_SERVICE_TYPE,
|
|
2389
|
+
GitHubActions,
|
|
2390
|
+
GitHubService,
|
|
2391
|
+
createGitHubConnectorAccountProvider,
|
|
2392
|
+
index_default as default,
|
|
2393
|
+
defaultGitHubAccountIdForRole,
|
|
2394
|
+
githubAction,
|
|
2395
|
+
githubPlugin,
|
|
2396
|
+
issueOpAction,
|
|
2397
|
+
normalizeGitHubAccountId,
|
|
2398
|
+
notificationTriageAction,
|
|
2399
|
+
prOpAction,
|
|
2400
|
+
readGitHubAccounts,
|
|
2401
|
+
readGitHubAccountsWithConnectorCredentials,
|
|
2402
|
+
resolveGitHubAccount,
|
|
2403
|
+
resolveGitHubAccountSelection,
|
|
2404
|
+
scoreNotification
|
|
2405
|
+
};
|