@vercel/board 0.0.0-stage → 0.15.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.
Files changed (126) hide show
  1. package/README.md +179 -2
  2. package/dist/a2-5EG8FkZ8.d.mts +3359 -0
  3. package/dist/a2-DYGYankn.mjs +3045 -0
  4. package/dist/access-CJU2-vaa.mjs +226 -0
  5. package/dist/access-CV96SYgr.d.mts +32 -0
  6. package/dist/access.d.mts +29 -0
  7. package/dist/access.mjs +4 -0
  8. package/dist/actors-rdN4dDTU.mjs +223 -0
  9. package/dist/attempts-BQUp9MKO.mjs +919 -0
  10. package/dist/auth-COzc09sn.mjs +276 -0
  11. package/dist/auth-wHFcAte3.d.mts +203 -0
  12. package/dist/backend-a2/blob-vercel.d.mts +8 -0
  13. package/dist/backend-a2/blob-vercel.mjs +81 -0
  14. package/dist/backend-a2/contract.d.mts +18 -0
  15. package/dist/backend-a2/contract.mjs +5 -0
  16. package/dist/backend-a2/scheduler-local.d.mts +11 -0
  17. package/dist/backend-a2/scheduler-local.mjs +2 -0
  18. package/dist/backend-a2/scheduler-qstash.d.mts +2 -0
  19. package/dist/backend-a2/scheduler-qstash.mjs +2 -0
  20. package/dist/backend-a2/scheduler-vercel.d.mts +2 -0
  21. package/dist/backend-a2/scheduler-vercel.mjs +2 -0
  22. package/dist/backend-a2/store-memory.d.mts +11 -0
  23. package/dist/backend-a2/store-memory.mjs +19 -0
  24. package/dist/backend-a2/store-postgres.d.mts +38 -0
  25. package/dist/backend-a2/store-postgres.mjs +110 -0
  26. package/dist/backend-a2/store-redis-http.d.mts +17 -0
  27. package/dist/backend-a2/store-redis-http.mjs +32 -0
  28. package/dist/backend-a2/store-redis.d.mts +23 -0
  29. package/dist/backend-a2/store-redis.mjs +39 -0
  30. package/dist/backend-a2/store-sqlite.d.mts +12 -0
  31. package/dist/backend-a2/store-sqlite.mjs +31 -0
  32. package/dist/backend-a2.d.mts +4 -0
  33. package/dist/backend-a2.mjs +2 -0
  34. package/dist/backend-http-8ex5g7Wv.d.mts +630 -0
  35. package/dist/backend-http-BsDSgmeo.mjs +613 -0
  36. package/dist/backend-http-Cv6xbEy9.mjs +469 -0
  37. package/dist/backend-http.d.mts +10 -0
  38. package/dist/backend-http.mjs +2 -0
  39. package/dist/backend-http.node.d.mts +10 -0
  40. package/dist/backend-http.node.mjs +2 -0
  41. package/dist/blob-C0J3wbGv.d.mts +39 -0
  42. package/dist/bots-CGkO76qH.mjs +67 -0
  43. package/dist/chat-provider-XCnQlxq6.d.mts +140 -0
  44. package/dist/chat-provider-jFSeyNmB.mjs +389 -0
  45. package/dist/cli.d.mts +1 -0
  46. package/dist/cli.mjs +2252 -0
  47. package/dist/client-DGDPaN7U.mjs +380 -0
  48. package/dist/conformance-DRR23YNy.mjs +512 -0
  49. package/dist/conformance.d.mts +30 -0
  50. package/dist/conformance.mjs +2 -0
  51. package/dist/contract-CuJmlrhw.mjs +566 -0
  52. package/dist/emoji-CTLpvH-A.mjs +445 -0
  53. package/dist/engine-3EvQ8idk.mjs +1449 -0
  54. package/dist/errors-DX3hpwWt.mjs +64 -0
  55. package/dist/exports-BpgyEdh6.d.mts +4 -0
  56. package/dist/exports-CyY3_FjZ.mjs +1285 -0
  57. package/dist/files-SniSmeEr.mjs +47 -0
  58. package/dist/index.d.mts +13 -0
  59. package/dist/index.default-BZ3lItjH.mjs +12 -0
  60. package/dist/index.mjs +19 -0
  61. package/dist/index.node.d.mts +14 -0
  62. package/dist/index.node.mjs +5 -0
  63. package/dist/inspect/app.js +416 -0
  64. package/dist/inspect/engine.js +5 -0
  65. package/dist/local-Cr_dSjvs.mjs +44 -0
  66. package/dist/own-OwgPWdmh.mjs +15 -0
  67. package/dist/project-CFiArraX.mjs +124 -0
  68. package/dist/provider-BGjz-mDa.d.mts +2473 -0
  69. package/dist/provider-CuewcDNZ.mjs +205 -0
  70. package/dist/redis-tables-B35vv4CZ.mjs +842 -0
  71. package/dist/reducers-B_vUGQRp.d.mts +1172 -0
  72. package/dist/reducers-mUPFuFJV.mjs +293 -0
  73. package/dist/schemas-Be08nyfW.mjs +74 -0
  74. package/dist/schemas-CWxs3ECJ.d.mts +50 -0
  75. package/dist/serve-Ccbb2LSb.mjs +170 -0
  76. package/dist/server-Cxey38B-.mjs +17 -0
  77. package/dist/serving-BNbzc96L.mjs +2 -0
  78. package/dist/sql-DbiEnqM0.mjs +571 -0
  79. package/dist/sqlite-db-Cbj03vmp.mjs +70 -0
  80. package/dist/sync-github.d.mts +473 -0
  81. package/dist/sync-github.mjs +603 -0
  82. package/dist/sync-kapso.d.mts +175 -0
  83. package/dist/sync-kapso.mjs +86 -0
  84. package/dist/sync-slack.d.mts +364 -0
  85. package/dist/sync-slack.mjs +728 -0
  86. package/dist/sync.chat.d.mts +2 -0
  87. package/dist/sync.chat.mjs +2 -0
  88. package/dist/sync.d.mts +3 -0
  89. package/dist/sync.mjs +4 -0
  90. package/dist/targets-ojFEXEyN.mjs +94 -0
  91. package/dist/tokens-DKLn5P9B.mjs +256 -0
  92. package/dist/types-BHCjTZdB.d.mts +104 -0
  93. package/dist/users-Dls7_ANV.mjs +42 -0
  94. package/dist/watch-spec-W0c7ypHJ.mjs +787 -0
  95. package/dist/watchers-hash-D9hbHLKB.mjs +225 -0
  96. package/docs/01-board/01-mounting.md +506 -0
  97. package/docs/01-board/02-database.md +258 -0
  98. package/docs/01-board/03-identity.md +262 -0
  99. package/docs/01-board/04-messages.md +192 -0
  100. package/docs/01-board/05-reading.md +177 -0
  101. package/docs/01-board/06-watchers.md +249 -0
  102. package/docs/01-board/07-schedules.md +133 -0
  103. package/docs/01-board/08-middleware.md +139 -0
  104. package/docs/01-board/09-access.md +499 -0
  105. package/docs/01-quickstart.md +196 -0
  106. package/docs/02-sync/01-overview.md +290 -0
  107. package/docs/02-sync/02-callbacks.md +265 -0
  108. package/docs/02-sync/03-slack.md +248 -0
  109. package/docs/02-sync/04-github.md +201 -0
  110. package/docs/02-sync/05-whatsapp.md +148 -0
  111. package/docs/02-sync/06-providers.md +228 -0
  112. package/docs/03-guides/01-coding-agents.md +226 -0
  113. package/docs/03-guides/02-inspector.md +104 -0
  114. package/docs/03-guides/03-ci.md +251 -0
  115. package/docs/03-guides/04-triage.md +233 -0
  116. package/docs/03-guides/05-agent-factory.md +142 -0
  117. package/docs/04-reference/01-cli.md +606 -0
  118. package/docs/04-reference/02-board.md +579 -0
  119. package/docs/04-reference/03-http.md +489 -0
  120. package/docs/04-reference/04-log.md +250 -0
  121. package/docs/04-reference/05-conformance.md +97 -0
  122. package/docs/index.md +186 -0
  123. package/package.json +165 -3
  124. package/sql/postgres/board.sql +145 -0
  125. package/sql/postgres/notifications.sql +24 -0
  126. package/sql/postgres/schema.sql +451 -0
@@ -0,0 +1,603 @@
1
+ import { c as eventSchema, s as envelope } from "./provider-CuewcDNZ.mjs";
2
+ import { r as withUserTokens, t as connectUserTokens } from "./users-Dls7_ANV.mjs";
3
+ import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-jFSeyNmB.mjs";
4
+ import { z } from "zod";
5
+ import { connectGitHubAdapter } from "@vercel/connect/chat";
6
+ import { createGitHubAdapter } from "@chat-adapter/github";
7
+ //#region src/sync/github.ts
8
+ /** `owner/repo:12` (and its review threads) → `owner/repo:pulls`; `owner/repo:issue:3` → `owner/repo:issues`. */
9
+ function channelOf(thread) {
10
+ const match = /^([^:]+):(issue:)?\d+/.exec(thread);
11
+ return match ? `${match[1]}:${match[2] ? "issues" : "pulls"}` : thread;
12
+ }
13
+ const author = (user) => ({
14
+ id: `github:user:${user?.id ?? "unknown"}`,
15
+ handle: user?.login ?? "unknown",
16
+ name: user?.login ?? "unknown",
17
+ avatar: user?.avatar_url ?? null,
18
+ isBot: user?.type === "Bot"
19
+ });
20
+ /** `good first issue` → `good-first-issue`: the part of a label key a name gives. Empty when nothing's left. */
21
+ const slug = (name) => name.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/^[-_.]+|-+$/g, "");
22
+ /**
23
+ * A GitHub label or assignee as a board label: `bug` → `tag.bug=bug`,
24
+ * `julianbenegas` → `assignee.julianbenegas=julianbenegas`. The keys don't
25
+ * name GitHub, so a filter like `--label tag.bug` holds for any platform.
26
+ */
27
+ function tagOf(prefix, name) {
28
+ const key = name ? slug(name) : "";
29
+ return key ? [`${prefix}.${key}`.slice(0, 64), name.slice(0, 256)] : null;
30
+ }
31
+ /** A PR's or issue's current labels and assignees, as board labels. */
32
+ function tagsOf(item) {
33
+ const tags = [...(item.labels ?? []).map((label) => tagOf("tag", label.name)), ...(item.assignees ?? []).map((user) => tagOf("assignee", user.login))];
34
+ return Object.fromEntries(tags.filter((tag) => tag !== null));
35
+ }
36
+ /** Actions that change only labels and assignees, which every PR and issue event reconciles. */
37
+ const TAG_ACTIONS = [
38
+ "labeled",
39
+ "unlabeled",
40
+ "assigned",
41
+ "unassigned"
42
+ ];
43
+ /**
44
+ * The label key prefixes the mirror owns on a GitHub root: keys under them that GitHub doesn't have are unset.
45
+ * A prefix is owned only when the payload carries its array, so a trimmed payload doesn't unset every key.
46
+ */
47
+ function ownedBy(item) {
48
+ return [...Array.isArray(item.labels) ? ["tag."] : [], ...Array.isArray(item.assignees) ? ["assignee."] : []];
49
+ }
50
+ /** Maps a raw `pull_request`, `pull_request_review`, or `issues` webhook to a lifecycle event. */
51
+ function lifecycle(name, body) {
52
+ const action = String(body.action ?? "");
53
+ const sender = body.sender;
54
+ if (name === "pull_request") {
55
+ const pr = body.pull_request;
56
+ const pr_label = { pr: `${body.repository.full_name}#${pr.number}` };
57
+ const base = {
58
+ title: `PR #${pr.number}: ${pr.title}`,
59
+ url: pr.html_url,
60
+ unset: []
61
+ };
62
+ if (TAG_ACTIONS.includes(action)) return {
63
+ number: pr.number,
64
+ user: sender ?? {},
65
+ lifecycle: {
66
+ ...base,
67
+ type: `pr.${action}`,
68
+ labels: {},
69
+ reply: null,
70
+ createRoot: false
71
+ }
72
+ };
73
+ switch (action) {
74
+ case "opened": return {
75
+ number: pr.number,
76
+ user: sender ?? {},
77
+ lifecycle: {
78
+ ...base,
79
+ type: "pr.opened",
80
+ labels: {
81
+ ...pr_label,
82
+ state: pr.draft ? "draft" : "open"
83
+ },
84
+ reply: null,
85
+ createRoot: true
86
+ }
87
+ };
88
+ case "ready_for_review": return {
89
+ number: pr.number,
90
+ user: sender ?? {},
91
+ lifecycle: {
92
+ ...base,
93
+ type: "pr.ready",
94
+ labels: {
95
+ ...pr_label,
96
+ state: "open"
97
+ },
98
+ reply: null,
99
+ createRoot: true
100
+ }
101
+ };
102
+ case "converted_to_draft": return {
103
+ number: pr.number,
104
+ user: sender ?? {},
105
+ lifecycle: {
106
+ ...base,
107
+ type: "pr.draft",
108
+ labels: { state: "draft" },
109
+ reply: null,
110
+ createRoot: false
111
+ }
112
+ };
113
+ case "reopened": return {
114
+ number: pr.number,
115
+ user: sender ?? {},
116
+ lifecycle: {
117
+ ...base,
118
+ type: "pr.reopened",
119
+ labels: {
120
+ ...pr_label,
121
+ state: pr.draft ? "draft" : "open"
122
+ },
123
+ reply: `reopened by ${sender?.login ?? "someone"}`,
124
+ createRoot: true
125
+ }
126
+ };
127
+ case "closed": return {
128
+ number: pr.number,
129
+ user: sender ?? {},
130
+ lifecycle: {
131
+ ...base,
132
+ type: "pr.closed",
133
+ labels: { state: pr.merged ? "merged" : "closed" },
134
+ reply: `${pr.merged ? "merged" : "closed"} by ${sender?.login ?? "someone"}`,
135
+ createRoot: false
136
+ }
137
+ };
138
+ default: return null;
139
+ }
140
+ }
141
+ if (name === "pull_request_review") {
142
+ const pr = body.pull_request;
143
+ const review = body.review;
144
+ const base = {
145
+ title: `PR #${pr.number}: ${pr.title}`,
146
+ url: pr.html_url,
147
+ createRoot: false
148
+ };
149
+ if (action === "dismissed") return {
150
+ number: pr.number,
151
+ user: sender ?? {},
152
+ lifecycle: {
153
+ ...base,
154
+ type: "review.dismissed",
155
+ labels: {},
156
+ unset: ["review"],
157
+ reply: null
158
+ }
159
+ };
160
+ if (action !== "submitted") return null;
161
+ const state = String(review.state ?? "").toLowerCase();
162
+ const verdict = state === "approved" ? "approved" : state === "changes_requested" ? "changes requested" : null;
163
+ const text = [verdict ? `${verdict} by ${sender?.login ?? "someone"}` : null, review.body || null].filter(Boolean).join("\n\n");
164
+ if (!text) return null;
165
+ return {
166
+ number: pr.number,
167
+ user: sender ?? {},
168
+ lifecycle: {
169
+ ...base,
170
+ type: "review.submitted",
171
+ labels: verdict ? { review: state } : {},
172
+ unset: [],
173
+ reply: text
174
+ }
175
+ };
176
+ }
177
+ if (name === "issues") {
178
+ const issue = body.issue;
179
+ const label = { issue: `${body.repository.full_name}#${issue.number}` };
180
+ const base = {
181
+ title: `Issue #${issue.number}: ${issue.title}`,
182
+ url: issue.html_url,
183
+ unset: []
184
+ };
185
+ if (TAG_ACTIONS.includes(action)) return {
186
+ number: issue.number,
187
+ user: sender ?? {},
188
+ lifecycle: {
189
+ ...base,
190
+ type: `issue.${action}`,
191
+ labels: {},
192
+ reply: null,
193
+ createRoot: false
194
+ }
195
+ };
196
+ if (action === "opened") return {
197
+ number: issue.number,
198
+ user: sender ?? {},
199
+ lifecycle: {
200
+ ...base,
201
+ type: "issue.opened",
202
+ labels: {
203
+ ...label,
204
+ state: "open"
205
+ },
206
+ reply: null,
207
+ createRoot: true
208
+ }
209
+ };
210
+ if (action === "closed") return {
211
+ number: issue.number,
212
+ user: sender ?? {},
213
+ lifecycle: {
214
+ ...base,
215
+ type: "issue.closed",
216
+ labels: { state: "closed" },
217
+ reply: `closed by ${sender?.login ?? "someone"}`,
218
+ createRoot: false
219
+ }
220
+ };
221
+ if (action === "reopened") return {
222
+ number: issue.number,
223
+ user: sender ?? {},
224
+ lifecycle: {
225
+ ...base,
226
+ type: "issue.reopened",
227
+ labels: {
228
+ ...label,
229
+ state: "open"
230
+ },
231
+ reply: `reopened by ${sender?.login ?? "someone"}`,
232
+ createRoot: true
233
+ }
234
+ };
235
+ }
236
+ return null;
237
+ }
238
+ const GitHubSubject = z.object({
239
+ type: z.enum(["pull_request", "issue"]),
240
+ repo: z.string(),
241
+ number: z.number().int().positive()
242
+ });
243
+ const NativeItem = z.looseObject({
244
+ number: z.number(),
245
+ title: z.string(),
246
+ html_url: z.string(),
247
+ body: z.string().nullable().optional(),
248
+ draft: z.boolean().optional(),
249
+ state: z.string().optional(),
250
+ base: z.looseObject({ ref: z.string() }).optional(),
251
+ head: z.looseObject({ ref: z.string() }).optional()
252
+ });
253
+ const GitHubPayload = z.looseObject({
254
+ pull_request: NativeItem.optional(),
255
+ issue: NativeItem.optional(),
256
+ action: z.string().optional()
257
+ });
258
+ const prTypes = [
259
+ "pr.opened",
260
+ "pr.ready",
261
+ "pr.draft",
262
+ "pr.reopened",
263
+ "pr.closed",
264
+ "pr.labeled",
265
+ "pr.unlabeled",
266
+ "pr.assigned",
267
+ "pr.unassigned",
268
+ "review.submitted",
269
+ "review.dismissed"
270
+ ];
271
+ const issueTypes = [
272
+ "issue.opened",
273
+ "issue.closed",
274
+ "issue.reopened",
275
+ "issue.labeled",
276
+ "issue.unlabeled",
277
+ "issue.assigned",
278
+ "issue.unassigned"
279
+ ];
280
+ const [messageEvent, editedEvent, deletedEvent, reactionEvent, lifecycleEvent] = eventSchema("github", "comment").options;
281
+ const githubFields = {
282
+ subject: GitHubSubject,
283
+ raw: GitHubPayload
284
+ };
285
+ const prLifecycle = (type) => lifecycleEvent.extend({
286
+ type: z.literal(type),
287
+ subject: GitHubSubject.extend({ type: z.literal("pull_request") }),
288
+ raw: GitHubPayload.extend({ pull_request: NativeItem })
289
+ });
290
+ const issueLifecycle = (type) => lifecycleEvent.extend({
291
+ type: z.literal(type),
292
+ subject: GitHubSubject.extend({ type: z.literal("issue") }),
293
+ raw: GitHubPayload.extend({ issue: NativeItem })
294
+ });
295
+ const GitHubEvent = z.discriminatedUnion("type", [
296
+ messageEvent.extend(githubFields),
297
+ editedEvent.extend(githubFields),
298
+ deletedEvent.extend(githubFields),
299
+ reactionEvent.extend(githubFields),
300
+ ...prTypes.map(prLifecycle),
301
+ ...issueTypes.map(issueLifecycle)
302
+ ]);
303
+ /** `owner/repo:12`, `owner/repo:issue:3`, or `owner/repo:12:rc:345` (a review comment thread). */
304
+ function parseThread(thread) {
305
+ const match = /^([^/]+)\/([^:]+):(?:issue:)?(\d+)(?::rc:(\d+))?$/.exec(thread);
306
+ if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
307
+ return {
308
+ owner: match[1],
309
+ repo: match[2],
310
+ number: Number(match[3]),
311
+ review: match[4] ? Number(match[4]) : null
312
+ };
313
+ }
314
+ /** GitHub's REST API with a person's own token: Chat SDK's adapter only holds the app's. */
315
+ async function asPerson(token, method, path, body) {
316
+ const response = await fetch(`https://api.github.com${path}`, {
317
+ method,
318
+ headers: {
319
+ authorization: `Bearer ${token}`,
320
+ accept: "application/vnd.github+json",
321
+ "x-github-api-version": "2022-11-28",
322
+ ...body === void 0 ? {} : { "content-type": "application/json" }
323
+ },
324
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
325
+ });
326
+ if (!response.ok) {
327
+ const detail = await response.text().catch(() => "");
328
+ throw Object.assign(/* @__PURE__ */ new Error(`GitHub ${method} ${path}: ${response.status} ${detail.slice(0, 200)}`), { data: { error: `github_${response.status}` } });
329
+ }
330
+ return response.status === 204 ? null : response.json();
331
+ }
332
+ /**
333
+ * Syncs GitHub repos with board channels. Keys are repos (`'owner/repo'`);
334
+ * values are a board channel (PRs and issues become threads there), a
335
+ * function, or `{ pulls, issues }` to send each somewhere else. Roots never
336
+ * flow into GitHub: a board thread doesn't open an issue.
337
+ */
338
+ function github(options) {
339
+ return new GitHubProvider(options);
340
+ }
341
+ /**
342
+ * A connector's GitHub adapter, finding its own bot user ID. Chat SDK skips detection
343
+ * with Connect's installation token, since REST's `/user` refuses it, and only learns
344
+ * the ID from the first comment it posts, per instance. GraphQL's `viewer` answers with
345
+ * the app's bot user for the same token.
346
+ */
347
+ function fromConnector(connector, botUserId) {
348
+ const adapter = createGitHubAdapter({
349
+ ...connectGitHubAdapter(connector),
350
+ ...botUserId !== void 0 ? { botUserId } : {}
351
+ });
352
+ const detecting = adapter;
353
+ detecting.detectBotUserId = async (octokit) => {
354
+ if (detecting._botUserId !== null || !octokit.graphql) return;
355
+ try {
356
+ const { viewer } = await octokit.graphql("query { viewer { databaseId login } }");
357
+ if (typeof viewer?.databaseId !== "number") return;
358
+ detecting._botUserId = viewer.databaseId;
359
+ detecting.logger?.info("GitHub bot user ID detected from the GraphQL viewer", {
360
+ botUserId: viewer.databaseId,
361
+ login: viewer.login
362
+ });
363
+ } catch (error) {
364
+ detecting.logger?.warn("Could not detect the GitHub bot user ID", { error });
365
+ }
366
+ };
367
+ return withUserTokens(adapter, connectUserTokens(connector));
368
+ }
369
+ var GitHubProvider = class extends ChatSyncProvider {
370
+ body;
371
+ constructor(given) {
372
+ const options = {
373
+ ...given,
374
+ adapter: typeof given.adapter === "string" ? fromConnector(given.adapter, given.botUserId) : given.adapter
375
+ };
376
+ super({
377
+ render: options.render,
378
+ platform: "github",
379
+ adapter: options.adapter,
380
+ canStartThreads: false,
381
+ mention: (actor) => actor.id.startsWith("github:user:") ? `@${actor.handle}` : null,
382
+ resolveChannel: (key) => Promise.resolve(key),
383
+ groupThread: (thread) => thread.replace(/:rc:[^:]+$/, ""),
384
+ channelOf,
385
+ isRoot: () => false,
386
+ async fetchRoot(thread) {
387
+ const match = /^([^/]+)\/([^:]+):(issue:)?(\d+)$/.exec(thread);
388
+ if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
389
+ const [, owner, repo, issue, number] = match;
390
+ const client = options.adapter.octokit.rest;
391
+ const { data } = issue ? await client.issues.get({
392
+ owner,
393
+ repo,
394
+ issue_number: Number(number)
395
+ }) : await client.pulls.get({
396
+ owner,
397
+ repo,
398
+ pull_number: Number(number)
399
+ });
400
+ return {
401
+ text: `${issue ? "Issue" : "PR"} #${data.number}: ${data.title}\n${data.html_url}`,
402
+ raw: issue ? { issue: data } : { pull_request: data },
403
+ author: author(data.user),
404
+ labels: {
405
+ ...tagsOf(data),
406
+ [issue ? "issue" : "pr"]: `${owner}/${repo}#${data.number}`,
407
+ state: issue ? data.state : data.merged ? "merged" : data.state === "closed" ? "closed" : data.draft ? "draft" : "open"
408
+ }
409
+ };
410
+ },
411
+ markdown(message) {
412
+ const raw = message.raw;
413
+ const body = raw?.comment?.body ?? raw?.body;
414
+ return typeof body === "string" && body.trim() ? body.trim() : markdownOf(message);
415
+ },
416
+ async avatar(message) {
417
+ const raw = message.raw;
418
+ const url = raw?.comment?.user?.avatar_url ?? raw?.user?.avatar_url;
419
+ return typeof url === "string" ? url : null;
420
+ },
421
+ context(message) {
422
+ const raw = message.raw;
423
+ const path = raw?.path ?? raw?.comment?.path;
424
+ const line = raw?.line ?? raw?.comment?.line;
425
+ return path ? line ? `${path}:${line}` : path : null;
426
+ },
427
+ missed(headers, body) {
428
+ const name = headers.get("x-github-event") ?? "";
429
+ if (name !== "issue_comment" && name !== "pull_request_review_comment") return [];
430
+ let payload;
431
+ try {
432
+ payload = JSON.parse(body);
433
+ } catch {
434
+ return [];
435
+ }
436
+ if (payload.action !== "edited" && payload.action !== "deleted" || !payload.comment?.id || !payload.repository) return [];
437
+ const { comment, repository } = payload;
438
+ const adapter = options.adapter;
439
+ const review = name === "pull_request_review_comment";
440
+ const number = review ? payload.pull_request?.number : payload.issue?.number;
441
+ if (!number) return [];
442
+ const type = review ? void 0 : payload.issue?.pull_request ? "pr" : "issue";
443
+ const threadId = adapter.encodeThreadId({
444
+ owner: repository.owner.login,
445
+ repo: repository.name,
446
+ prNumber: number,
447
+ ...review ? { reviewCommentId: comment.in_reply_to_id ?? comment.id } : { type }
448
+ });
449
+ const message = review ? adapter.parseReviewComment(comment, repository, number, threadId) : adapter.parseIssueComment(comment, repository, number, threadId, type);
450
+ if (message.author.isMe) return [];
451
+ return payload.action === "edited" ? [{
452
+ kind: "edited",
453
+ message
454
+ }] : [{
455
+ kind: "deleted",
456
+ thread: threadId,
457
+ message: message.id,
458
+ raw: payload
459
+ }];
460
+ },
461
+ intercept(key, headers, body) {
462
+ const name = headers.get("x-github-event") ?? "";
463
+ if (![
464
+ "pull_request",
465
+ "pull_request_review",
466
+ "issues"
467
+ ].includes(name)) return [];
468
+ let parsed;
469
+ try {
470
+ parsed = JSON.parse(body);
471
+ } catch {
472
+ return [];
473
+ }
474
+ const found = lifecycle(name, parsed);
475
+ if (!found) return [];
476
+ const item = parsed.pull_request ?? parsed.issue;
477
+ if (item) Object.assign(found.lifecycle, {
478
+ labels: {
479
+ ...tagsOf(item),
480
+ ...found.lifecycle.labels
481
+ },
482
+ owns: ownedBy(item)
483
+ });
484
+ const repo = parsed.repository.full_name;
485
+ const thread = name === "issues" ? `${key}:${repo}:issue:${found.number}` : `${key}:${repo}:${found.number}`;
486
+ const delivery = headers.get("x-github-delivery") ?? `${name}:${found.lifecycle.type}:${found.number}`;
487
+ return [{
488
+ event: {
489
+ kind: "lifecycle",
490
+ channel: `${key}:${repo}:${name === "issues" ? "issues" : "pulls"}`,
491
+ thread,
492
+ author: author(found.user),
493
+ ...found.lifecycle
494
+ },
495
+ raw: parsed,
496
+ dedupe: `lifecycle:${delivery}`
497
+ }];
498
+ },
499
+ async resolveTarget(target) {
500
+ if (!/^[^/]+\/[^:]+(?::(?:issue:)?\d+(?::rc:[^:]+)?)?$/.test(target)) throw new Error(`invalid GitHub target: ${target}`);
501
+ const at = target.indexOf(":");
502
+ return {
503
+ target,
504
+ channel: at === -1 ? target : channelOf(target),
505
+ kind: at === -1 ? "channel" : "thread"
506
+ };
507
+ }
508
+ });
509
+ const converter = options.adapter.formatConverter;
510
+ this.body = (post) => {
511
+ try {
512
+ return converter ? converter.renderPostable({ markdown: post.markdown }) : post.markdown;
513
+ } catch {
514
+ return post.markdown;
515
+ }
516
+ };
517
+ for (const [repo, value] of Object.entries(options.repos)) {
518
+ const { pulls, issues } = typeof value === "object" && !("run" in value) ? value : {
519
+ pulls: value,
520
+ issues: value
521
+ };
522
+ if (pulls) this.map(`${repo}:pulls`, pulls);
523
+ if (issues) this.map(`${repo}:issues`, issues);
524
+ }
525
+ }
526
+ async whoAmI(token) {
527
+ const user = await asPerson(token, "GET", "/user");
528
+ if (!user.id) throw new Error("GitHub didn't say whose token this is");
529
+ return {
530
+ ...author(user),
531
+ isBot: false
532
+ };
533
+ }
534
+ async postAs(token, target, kind, post) {
535
+ if (kind === "channel") throw new Error("github cannot start threads");
536
+ const { owner, repo, number, review } = parseThread(target);
537
+ const body = this.body(post);
538
+ const comment = await (review !== null ? asPerson(token, "POST", `/repos/${owner}/${repo}/pulls/${number}/comments/${review}/replies`, { body }) : asPerson(token, "POST", `/repos/${owner}/${repo}/issues/${number}/comments`, { body }));
539
+ return {
540
+ thread: target,
541
+ message: String(comment.id)
542
+ };
543
+ }
544
+ async editAs(token, thread, message, post) {
545
+ const { owner, repo, review } = parseThread(thread);
546
+ await asPerson(token, "PATCH", `/repos/${owner}/${repo}/${review !== null ? "pulls" : "issues"}/comments/${message}`, { body: this.body(post) });
547
+ }
548
+ async deleteAs(token, thread, message) {
549
+ const { owner, repo, review } = parseThread(thread);
550
+ await asPerson(token, "DELETE", `/repos/${owner}/${repo}/${review !== null ? "pulls" : "issues"}/comments/${message}`);
551
+ }
552
+ /**
553
+ * A thread's root is the PR or issue itself, not a comment: its copy has no comment ID
554
+ * (`root:<event>` or `lifecycle:<event>`), so its reactions go on the PR or issue.
555
+ */
556
+ async react(thread, message, emoji, added) {
557
+ if (/^\d+$/.test(message)) return super.react(thread, message, emoji, added);
558
+ const match = /^([^/]+)\/([^:]+):(?:issue:)?(\d+)$/.exec(thread);
559
+ if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
560
+ const adapter = await this.ready();
561
+ const content = adapter.emojiToGitHubReaction?.(emoji);
562
+ if (!content || content === "+1" && emoji !== "thumbs_up" && emoji !== "+1") return;
563
+ const [, owner, repo, number] = match;
564
+ const reactions = (await adapter.getOctokitForThread?.(owner, repo) ?? adapter.octokit).rest.reactions;
565
+ if (!reactions) throw new Error("GitHub client has no reactions API");
566
+ const where = {
567
+ owner,
568
+ repo,
569
+ issue_number: Number(number)
570
+ };
571
+ if (added) return void await reactions.createForIssue({
572
+ ...where,
573
+ content
574
+ });
575
+ if (adapter.botUserId === void 0) return;
576
+ const { data } = await reactions.listForIssue({
577
+ ...where,
578
+ content,
579
+ per_page: 100
580
+ });
581
+ const own = data.find((reaction) => reaction.content === content && String(reaction.user?.id) === adapter.botUserId);
582
+ if (own) await reactions.deleteForIssue({
583
+ ...where,
584
+ reaction_id: own.id
585
+ });
586
+ }
587
+ event(context) {
588
+ const match = /^([^:]+):(issue:)?(\d+)/.exec(context.event.thread.slice(context.key.length + 1));
589
+ if (!match) throw new Error(`invalid GitHub thread: ${context.event.thread}`);
590
+ const base = envelope(this.platform, context.key, context.event, context.raw ?? {}, context.id);
591
+ return GitHubEvent.parse({
592
+ ...base,
593
+ type: context.event.kind === "message" ? "comment" : base.type,
594
+ subject: {
595
+ type: match[2] ? "issue" : "pull_request",
596
+ repo: match[1],
597
+ number: Number(match[3])
598
+ }
599
+ });
600
+ }
601
+ };
602
+ //#endregion
603
+ export { GitHubEvent, GitHubPayload, GitHubProvider, github, withUserTokens };