@holon-run/agentinbox 0.1.0
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/LICENSE +178 -0
- package/README.md +168 -0
- package/dist/src/adapters.js +111 -0
- package/dist/src/backend.js +175 -0
- package/dist/src/cli.js +620 -0
- package/dist/src/client.js +279 -0
- package/dist/src/control_server.js +93 -0
- package/dist/src/daemon.js +246 -0
- package/dist/src/filter.js +167 -0
- package/dist/src/http.js +408 -0
- package/dist/src/matcher.js +47 -0
- package/dist/src/model.js +2 -0
- package/dist/src/paths.js +141 -0
- package/dist/src/service.js +1338 -0
- package/dist/src/source_schema.js +150 -0
- package/dist/src/sources/feishu.js +567 -0
- package/dist/src/sources/github.js +485 -0
- package/dist/src/sources/github_ci.js +372 -0
- package/dist/src/store.js +1271 -0
- package/dist/src/terminal.js +301 -0
- package/dist/src/util.js +36 -0
- package/package.json +52 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GithubDeliveryAdapter = exports.GithubSourceRuntime = exports.GithubUxcClient = void 0;
|
|
4
|
+
exports.normalizeGithubRepoEvent = normalizeGithubRepoEvent;
|
|
5
|
+
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
6
|
+
const GITHUB_ENDPOINT = "https://api.github.com";
|
|
7
|
+
const DEFAULT_EVENT_TYPES = [
|
|
8
|
+
"IssuesEvent",
|
|
9
|
+
"IssueCommentEvent",
|
|
10
|
+
"PullRequestEvent",
|
|
11
|
+
"PullRequestReviewCommentEvent",
|
|
12
|
+
];
|
|
13
|
+
const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
|
|
14
|
+
class GithubUxcClient {
|
|
15
|
+
client;
|
|
16
|
+
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
|
|
17
|
+
this.client = client;
|
|
18
|
+
}
|
|
19
|
+
async ensureRepoEventsSubscription(config, checkpoint) {
|
|
20
|
+
if (checkpoint.uxcJobId) {
|
|
21
|
+
try {
|
|
22
|
+
const status = await this.client.subscribeStatus(checkpoint.uxcJobId);
|
|
23
|
+
if (status.status === "running" || status.status === "reconnecting") {
|
|
24
|
+
return {
|
|
25
|
+
job_id: checkpoint.uxcJobId,
|
|
26
|
+
mode: "poll",
|
|
27
|
+
protocol: "openapi",
|
|
28
|
+
endpoint: GITHUB_ENDPOINT,
|
|
29
|
+
sink: "memory:",
|
|
30
|
+
status: status.status,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Recreate the job below.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const started = await this.client.subscribeStart({
|
|
39
|
+
endpoint: GITHUB_ENDPOINT,
|
|
40
|
+
operationId: "get:/repos/{owner}/{repo}/events",
|
|
41
|
+
args: { owner: config.owner, repo: config.repo, per_page: config.perPage ?? 10 },
|
|
42
|
+
mode: "poll",
|
|
43
|
+
pollConfig: {
|
|
44
|
+
interval_secs: config.pollIntervalSecs ?? 30,
|
|
45
|
+
extract_items_pointer: "",
|
|
46
|
+
checkpoint_strategy: {
|
|
47
|
+
type: "item_key",
|
|
48
|
+
item_key_pointer: "/id",
|
|
49
|
+
seen_window: 1024,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
options: { auth: config.uxcAuth },
|
|
53
|
+
sink: "memory:",
|
|
54
|
+
ephemeral: false,
|
|
55
|
+
});
|
|
56
|
+
return started;
|
|
57
|
+
}
|
|
58
|
+
async readSubscriptionEvents(jobId, afterSeq) {
|
|
59
|
+
const response = await this.client.subscriptionEvents({
|
|
60
|
+
jobId,
|
|
61
|
+
afterSeq,
|
|
62
|
+
limit: 100,
|
|
63
|
+
waitMs: 10,
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
events: response.events,
|
|
67
|
+
nextAfterSeq: response.next_after_seq,
|
|
68
|
+
status: response.status,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async createIssueComment(input) {
|
|
72
|
+
await this.client.call({
|
|
73
|
+
endpoint: GITHUB_ENDPOINT,
|
|
74
|
+
operation: "post:/repos/{owner}/{repo}/issues/{issue_number}/comments",
|
|
75
|
+
payload: {
|
|
76
|
+
owner: input.owner,
|
|
77
|
+
repo: input.repo,
|
|
78
|
+
issue_number: input.issueNumber,
|
|
79
|
+
body: input.body,
|
|
80
|
+
},
|
|
81
|
+
options: { auth: input.auth },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async replyToReviewComment(input) {
|
|
85
|
+
await this.client.call({
|
|
86
|
+
endpoint: GITHUB_ENDPOINT,
|
|
87
|
+
operation: "post:/repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies",
|
|
88
|
+
payload: {
|
|
89
|
+
owner: input.owner,
|
|
90
|
+
repo: input.repo,
|
|
91
|
+
pull_number: input.pullNumber,
|
|
92
|
+
comment_id: input.commentId,
|
|
93
|
+
body: input.body,
|
|
94
|
+
},
|
|
95
|
+
options: { auth: input.auth },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.GithubUxcClient = GithubUxcClient;
|
|
100
|
+
class GithubSourceRuntime {
|
|
101
|
+
store;
|
|
102
|
+
appendSourceEvent;
|
|
103
|
+
client;
|
|
104
|
+
interval = null;
|
|
105
|
+
inFlight = new Set();
|
|
106
|
+
errorCounts = new Map();
|
|
107
|
+
nextRetryAt = new Map();
|
|
108
|
+
constructor(store, appendSourceEvent, client) {
|
|
109
|
+
this.store = store;
|
|
110
|
+
this.appendSourceEvent = appendSourceEvent;
|
|
111
|
+
this.client = client ?? new GithubUxcClient();
|
|
112
|
+
}
|
|
113
|
+
async ensureSource(source) {
|
|
114
|
+
if (source.sourceType !== "github_repo") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const config = parseGithubSourceConfig(source);
|
|
118
|
+
const checkpoint = parseGithubCheckpoint(source.checkpoint);
|
|
119
|
+
const started = await this.client.ensureRepoEventsSubscription(config, checkpoint);
|
|
120
|
+
this.store.updateSourceRuntime(source.sourceId, {
|
|
121
|
+
status: "active",
|
|
122
|
+
checkpoint: JSON.stringify({
|
|
123
|
+
...checkpoint,
|
|
124
|
+
uxcJobId: started.job_id,
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async start() {
|
|
129
|
+
if (this.interval) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.interval = setInterval(() => {
|
|
133
|
+
void this.syncAll();
|
|
134
|
+
}, 2_000);
|
|
135
|
+
try {
|
|
136
|
+
await this.syncAll();
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
console.warn("github_repo initial sync failed:", error);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async stop() {
|
|
143
|
+
if (this.interval) {
|
|
144
|
+
clearInterval(this.interval);
|
|
145
|
+
this.interval = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async pollSource(sourceId) {
|
|
149
|
+
return this.syncSource(sourceId);
|
|
150
|
+
}
|
|
151
|
+
status() {
|
|
152
|
+
return {
|
|
153
|
+
activeSourceIds: Array.from(this.inFlight.values()).sort(),
|
|
154
|
+
erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
async syncAll() {
|
|
158
|
+
const sources = this.store
|
|
159
|
+
.listSources()
|
|
160
|
+
.filter((source) => source.sourceType === "github_repo" && source.status !== "paused");
|
|
161
|
+
for (const source of sources) {
|
|
162
|
+
try {
|
|
163
|
+
await this.syncSource(source.sourceId);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.warn(`github_repo sync failed for ${source.sourceId}:`, error);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async syncSource(sourceId) {
|
|
171
|
+
if (this.inFlight.has(sourceId)) {
|
|
172
|
+
return {
|
|
173
|
+
sourceId,
|
|
174
|
+
sourceType: "github_repo",
|
|
175
|
+
appended: 0,
|
|
176
|
+
deduped: 0,
|
|
177
|
+
eventsRead: 0,
|
|
178
|
+
note: "source sync already in flight",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
this.inFlight.add(sourceId);
|
|
182
|
+
try {
|
|
183
|
+
const source = this.store.getSource(sourceId);
|
|
184
|
+
if (!source) {
|
|
185
|
+
throw new Error(`unknown source: ${sourceId}`);
|
|
186
|
+
}
|
|
187
|
+
const config = parseGithubSourceConfig(source);
|
|
188
|
+
if (source.status === "error") {
|
|
189
|
+
const retryAt = this.nextRetryAt.get(sourceId) ?? 0;
|
|
190
|
+
if (Date.now() < retryAt) {
|
|
191
|
+
return {
|
|
192
|
+
sourceId,
|
|
193
|
+
sourceType: "github_repo",
|
|
194
|
+
appended: 0,
|
|
195
|
+
deduped: 0,
|
|
196
|
+
eventsRead: 0,
|
|
197
|
+
note: "error backoff not elapsed",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let checkpoint = parseGithubCheckpoint(source.checkpoint);
|
|
202
|
+
const subscription = await this.client.ensureRepoEventsSubscription(config, checkpoint);
|
|
203
|
+
if (subscription.job_id !== checkpoint.uxcJobId) {
|
|
204
|
+
checkpoint = { ...checkpoint, uxcJobId: subscription.job_id };
|
|
205
|
+
}
|
|
206
|
+
const batch = await this.client.readSubscriptionEvents(checkpoint.uxcJobId, checkpoint.afterSeq ?? 0);
|
|
207
|
+
let appended = 0;
|
|
208
|
+
let deduped = 0;
|
|
209
|
+
for (const event of batch.events) {
|
|
210
|
+
if (event.event_kind !== "data") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const normalized = normalizeGithubRepoEvent(source, config, event.data);
|
|
214
|
+
if (!normalized) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const result = await this.appendSourceEvent(normalized);
|
|
218
|
+
appended += result.appended;
|
|
219
|
+
deduped += result.deduped;
|
|
220
|
+
}
|
|
221
|
+
this.store.updateSourceRuntime(sourceId, {
|
|
222
|
+
status: "active",
|
|
223
|
+
checkpoint: JSON.stringify({
|
|
224
|
+
...checkpoint,
|
|
225
|
+
uxcJobId: checkpoint.uxcJobId,
|
|
226
|
+
afterSeq: batch.nextAfterSeq,
|
|
227
|
+
lastEventAt: new Date().toISOString(),
|
|
228
|
+
lastError: null,
|
|
229
|
+
}),
|
|
230
|
+
});
|
|
231
|
+
this.errorCounts.delete(sourceId);
|
|
232
|
+
this.nextRetryAt.delete(sourceId);
|
|
233
|
+
return {
|
|
234
|
+
sourceId,
|
|
235
|
+
sourceType: "github_repo",
|
|
236
|
+
appended,
|
|
237
|
+
deduped,
|
|
238
|
+
eventsRead: batch.events.length,
|
|
239
|
+
note: `subscription status=${batch.status}`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
const source = this.store.getSource(sourceId);
|
|
244
|
+
if (source) {
|
|
245
|
+
const checkpoint = parseGithubCheckpoint(source.checkpoint);
|
|
246
|
+
const config = parseGithubSourceConfig(source);
|
|
247
|
+
const nextErrorCount = (this.errorCounts.get(sourceId) ?? 0) + 1;
|
|
248
|
+
this.errorCounts.set(sourceId, nextErrorCount);
|
|
249
|
+
this.nextRetryAt.set(sourceId, Date.now() + computeErrorBackoffMs(config.pollIntervalSecs ?? 30, nextErrorCount));
|
|
250
|
+
this.store.updateSourceRuntime(sourceId, {
|
|
251
|
+
status: "error",
|
|
252
|
+
checkpoint: JSON.stringify({
|
|
253
|
+
...checkpoint,
|
|
254
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
this.inFlight.delete(sourceId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
exports.GithubSourceRuntime = GithubSourceRuntime;
|
|
266
|
+
function computeErrorBackoffMs(pollIntervalSecs, errorCount) {
|
|
267
|
+
const baseMs = Math.max(1, pollIntervalSecs) * 1000;
|
|
268
|
+
const multiplier = Math.min(2 ** Math.max(0, errorCount - 1), MAX_ERROR_BACKOFF_MULTIPLIER);
|
|
269
|
+
return baseMs * multiplier;
|
|
270
|
+
}
|
|
271
|
+
class GithubDeliveryAdapter {
|
|
272
|
+
client;
|
|
273
|
+
constructor(client) {
|
|
274
|
+
this.client = client ?? new GithubUxcClient();
|
|
275
|
+
}
|
|
276
|
+
async send(request, attempt) {
|
|
277
|
+
const payloadText = stringifyDeliveryPayload(request.payload);
|
|
278
|
+
const target = parseTargetRef(attempt.targetRef);
|
|
279
|
+
if (attempt.surface === "issue_comment" || attempt.surface === "pull_request_comment") {
|
|
280
|
+
await this.client.createIssueComment({
|
|
281
|
+
owner: target.owner,
|
|
282
|
+
repo: target.repo,
|
|
283
|
+
issueNumber: target.number,
|
|
284
|
+
body: payloadText,
|
|
285
|
+
});
|
|
286
|
+
return { status: "sent", note: "sent GitHub issue comment" };
|
|
287
|
+
}
|
|
288
|
+
if (attempt.surface === "review_comment") {
|
|
289
|
+
if (!attempt.threadRef || !attempt.threadRef.startsWith("review_comment:")) {
|
|
290
|
+
throw new Error("review_comment delivery requires threadRef=review_comment:<id>");
|
|
291
|
+
}
|
|
292
|
+
await this.client.replyToReviewComment({
|
|
293
|
+
owner: target.owner,
|
|
294
|
+
repo: target.repo,
|
|
295
|
+
pullNumber: target.number,
|
|
296
|
+
commentId: Number(attempt.threadRef.slice("review_comment:".length)),
|
|
297
|
+
body: payloadText,
|
|
298
|
+
});
|
|
299
|
+
return { status: "sent", note: "sent GitHub review comment reply" };
|
|
300
|
+
}
|
|
301
|
+
throw new Error(`unsupported GitHub surface: ${attempt.surface}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
exports.GithubDeliveryAdapter = GithubDeliveryAdapter;
|
|
305
|
+
function normalizeGithubRepoEvent(source, config, raw) {
|
|
306
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const event = raw;
|
|
310
|
+
const eventType = asString(event.type);
|
|
311
|
+
if (!eventType || !(config.eventTypes ?? DEFAULT_EVENT_TYPES).includes(eventType)) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const eventId = asString(event.id);
|
|
315
|
+
if (!eventId) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const actor = asRecord(event.actor);
|
|
319
|
+
const payload = asRecord(event.payload);
|
|
320
|
+
const repo = asRecord(event.repo);
|
|
321
|
+
const issue = asRecord(payload.issue);
|
|
322
|
+
const pullRequest = asRecord(payload.pull_request);
|
|
323
|
+
const comment = asRecord(payload.comment);
|
|
324
|
+
const action = asString(payload.action) ?? "observed";
|
|
325
|
+
const number = asNumber(issue.number) ?? asNumber(pullRequest.number);
|
|
326
|
+
const isPullRequest = eventType.startsWith("PullRequest");
|
|
327
|
+
const title = asString(issue.title) ?? asString(pullRequest.title) ?? null;
|
|
328
|
+
const body = asString(comment.body) ?? asString(issue.body) ?? asString(pullRequest.body) ?? null;
|
|
329
|
+
const labels = extractLabels(issue.labels);
|
|
330
|
+
const mentions = extractMentions(title, body);
|
|
331
|
+
const url = asString(comment.html_url) ??
|
|
332
|
+
asString(issue.html_url) ??
|
|
333
|
+
asString(pullRequest.html_url) ??
|
|
334
|
+
asString(event["url"]);
|
|
335
|
+
const deliveryHandle = buildGithubDeliveryHandle(config, eventType, number, comment);
|
|
336
|
+
return {
|
|
337
|
+
sourceId: source.sourceId,
|
|
338
|
+
sourceNativeId: `github_event:${eventId}`,
|
|
339
|
+
eventVariant: `${eventType}.${action}`,
|
|
340
|
+
occurredAt: asString(event.created_at) ?? new Date().toISOString(),
|
|
341
|
+
metadata: {
|
|
342
|
+
provider: "github",
|
|
343
|
+
owner: config.owner,
|
|
344
|
+
repo: config.repo,
|
|
345
|
+
repoFullName: asString(repo.name) ?? `${config.owner}/${config.repo}`,
|
|
346
|
+
eventType,
|
|
347
|
+
action,
|
|
348
|
+
number,
|
|
349
|
+
author: asString(actor.login) ?? null,
|
|
350
|
+
isPullRequest,
|
|
351
|
+
labels,
|
|
352
|
+
mentions,
|
|
353
|
+
title,
|
|
354
|
+
body,
|
|
355
|
+
url,
|
|
356
|
+
},
|
|
357
|
+
rawPayload: {
|
|
358
|
+
id: eventId,
|
|
359
|
+
type: eventType,
|
|
360
|
+
action,
|
|
361
|
+
actor: actor.login ?? null,
|
|
362
|
+
issue,
|
|
363
|
+
pull_request: pullRequest,
|
|
364
|
+
comment,
|
|
365
|
+
},
|
|
366
|
+
deliveryHandle,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function buildGithubDeliveryHandle(config, eventType, number, comment) {
|
|
370
|
+
if (!number) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
if (eventType === "PullRequestReviewCommentEvent") {
|
|
374
|
+
const commentId = asNumber(comment.id);
|
|
375
|
+
return {
|
|
376
|
+
provider: "github",
|
|
377
|
+
surface: "review_comment",
|
|
378
|
+
targetRef: `${config.owner}/${config.repo}#${number}`,
|
|
379
|
+
threadRef: commentId ? `review_comment:${commentId}` : null,
|
|
380
|
+
replyMode: "reply",
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return {
|
|
384
|
+
provider: "github",
|
|
385
|
+
surface: eventType === "PullRequestEvent" ? "pull_request_comment" : "issue_comment",
|
|
386
|
+
targetRef: `${config.owner}/${config.repo}#${number}`,
|
|
387
|
+
threadRef: null,
|
|
388
|
+
replyMode: "comment",
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function parseGithubSourceConfig(source) {
|
|
392
|
+
const config = source.config ?? {};
|
|
393
|
+
const owner = asString(config.owner);
|
|
394
|
+
const repo = asString(config.repo);
|
|
395
|
+
if (!owner || !repo) {
|
|
396
|
+
const [fallbackOwner, fallbackRepo] = source.sourceKey.split("/", 2);
|
|
397
|
+
if (!fallbackOwner || !fallbackRepo) {
|
|
398
|
+
throw new Error(`github_repo source requires config.owner and config.repo: ${source.sourceId}`);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
owner: fallbackOwner,
|
|
402
|
+
repo: fallbackRepo,
|
|
403
|
+
uxcAuth: asString(config.uxcAuth) ?? asString(config.credentialRef) ?? undefined,
|
|
404
|
+
pollIntervalSecs: asNumber(config.pollIntervalSecs) ?? 30,
|
|
405
|
+
perPage: asNumber(config.perPage) ?? 10,
|
|
406
|
+
eventTypes: asStringArray(config.eventTypes) ?? DEFAULT_EVENT_TYPES,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
owner,
|
|
411
|
+
repo,
|
|
412
|
+
uxcAuth: asString(config.uxcAuth) ?? asString(config.credentialRef) ?? undefined,
|
|
413
|
+
pollIntervalSecs: asNumber(config.pollIntervalSecs) ?? 30,
|
|
414
|
+
perPage: asNumber(config.perPage) ?? 10,
|
|
415
|
+
eventTypes: asStringArray(config.eventTypes) ?? DEFAULT_EVENT_TYPES,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
function parseGithubCheckpoint(checkpoint) {
|
|
419
|
+
if (!checkpoint) {
|
|
420
|
+
return {};
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
return JSON.parse(checkpoint);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return {};
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function extractLabels(raw) {
|
|
430
|
+
if (!Array.isArray(raw)) {
|
|
431
|
+
return [];
|
|
432
|
+
}
|
|
433
|
+
return raw
|
|
434
|
+
.map((value) => asRecord(value))
|
|
435
|
+
.map((value) => asString(value.name))
|
|
436
|
+
.filter((value) => Boolean(value));
|
|
437
|
+
}
|
|
438
|
+
function extractMentions(...values) {
|
|
439
|
+
const seen = new Set();
|
|
440
|
+
for (const value of values) {
|
|
441
|
+
if (!value) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const matches = value.match(/@([a-zA-Z0-9_-]+)/g) ?? [];
|
|
445
|
+
for (const match of matches) {
|
|
446
|
+
seen.add(match.slice(1));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return Array.from(seen.values()).sort();
|
|
450
|
+
}
|
|
451
|
+
function parseTargetRef(targetRef) {
|
|
452
|
+
const match = targetRef.match(/^([^/]+)\/([^#]+)#(\d+)$/);
|
|
453
|
+
if (!match) {
|
|
454
|
+
throw new Error(`invalid GitHub targetRef: ${targetRef}`);
|
|
455
|
+
}
|
|
456
|
+
return {
|
|
457
|
+
owner: match[1],
|
|
458
|
+
repo: match[2],
|
|
459
|
+
number: Number(match[3]),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function stringifyDeliveryPayload(payload) {
|
|
463
|
+
if (typeof payload.text === "string") {
|
|
464
|
+
return payload.text;
|
|
465
|
+
}
|
|
466
|
+
return JSON.stringify(payload, null, 2);
|
|
467
|
+
}
|
|
468
|
+
function asRecord(value) {
|
|
469
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
470
|
+
return {};
|
|
471
|
+
}
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
function asString(value) {
|
|
475
|
+
return typeof value === "string" ? value : null;
|
|
476
|
+
}
|
|
477
|
+
function asNumber(value) {
|
|
478
|
+
return typeof value === "number" ? value : null;
|
|
479
|
+
}
|
|
480
|
+
function asStringArray(value) {
|
|
481
|
+
if (!Array.isArray(value)) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
return value.filter((item) => typeof item === "string");
|
|
485
|
+
}
|