@vercel/board 0.16.0 → 0.17.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 (127) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-Bck5x1Kh.mjs} +969 -25
  3. package/dist/access.d.mts +1 -1
  4. package/dist/access.mjs +3 -3
  5. package/dist/{actors-DjMeZpnU.mjs → actors-BOamkEuz.mjs} +3 -73
  6. package/dist/attempts-CLG840Oj.mjs +20 -0
  7. package/dist/{auth-COzc09sn.mjs → auth-Drg9FiC2.mjs} +225 -2
  8. package/dist/backend-B-SJ7p6p.d.mts +680 -0
  9. package/dist/backend-a2/contract.d.mts +873 -2
  10. package/dist/backend-a2/contract.mjs +2 -2
  11. package/dist/backend-a2/scheduler-local.d.mts +1 -1
  12. package/dist/backend-a2/scheduler-local.mjs +1 -1
  13. package/dist/backend-a2/scheduler-qstash.d.mts +78 -1
  14. package/dist/backend-a2/scheduler-qstash.mjs +521 -1
  15. package/dist/backend-a2/scheduler-vercel.d.mts +37 -1
  16. package/dist/backend-a2/scheduler-vercel.mjs +203 -1
  17. package/dist/backend-a2/store-memory.d.mts +2 -2
  18. package/dist/backend-a2/store-memory.mjs +3 -3
  19. package/dist/backend-a2/store-postgres.d.mts +1 -1
  20. package/dist/backend-a2/store-postgres.mjs +2850 -2
  21. package/dist/backend-a2/store-redis-http.d.mts +1 -1
  22. package/dist/backend-a2/store-redis-http.mjs +490 -2
  23. package/dist/backend-a2/store-redis.d.mts +10 -2
  24. package/dist/backend-a2/store-redis.mjs +51 -2
  25. package/dist/backend-a2/store-sqlite.d.mts +1 -1
  26. package/dist/backend-a2/store-sqlite.mjs +3 -3
  27. package/dist/backend-a2.d.mts +2706 -3
  28. package/dist/backend-a2.mjs +1 -1
  29. package/dist/{backend-http-Bid7RIpC.d.mts → backend-http-7cNb8nJI.d.mts} +10 -3
  30. package/dist/{backend-http-0roEmjl0.mjs → backend-http-C5I7Wfdw.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-CuUIACCs.mjs} +32 -3
  32. package/dist/backend-http.d.mts +1 -1
  33. package/dist/backend-http.mjs +1 -1
  34. package/dist/backend-http.node.d.mts +1 -1
  35. package/dist/backend-http.node.mjs +1 -1
  36. package/dist/{chat-provider-XCnQlxq6.d.mts → chat-provider-DNJQyajS.d.mts} +16 -4
  37. package/dist/{chat-provider-jFSeyNmB.mjs → chat-provider-YHy-WbrB.mjs} +15 -9
  38. package/dist/cli.mjs +30 -10
  39. package/dist/{client-DGDPaN7U.mjs → client-BA-J7usH.mjs} +10 -2
  40. package/dist/{conformance-DRR23YNy.mjs → conformance-CT5EeHeC.mjs} +5 -2
  41. package/dist/conformance.d.mts +1 -1
  42. package/dist/conformance.mjs +1 -1
  43. package/dist/{engine-Bir5s8ts.mjs → engine-D0oBUBEK.mjs} +249 -163
  44. package/dist/events-CewD4kw7.mjs +75 -0
  45. package/dist/{exports-Dvc1o52I.mjs → exports-DOdA7KhF.mjs} +35 -32
  46. package/dist/exports-ZsgjzEnM.d.mts +4 -0
  47. package/dist/external-BV_cUncs.mjs +151 -0
  48. package/dist/folds-CRYY14kc.d.mts +534 -0
  49. package/dist/{reducers-mUPFuFJV.mjs → folds-Dyjput8M.mjs} +6 -73
  50. package/dist/ids-C6AYo1bu.mjs +31 -0
  51. package/dist/index.d.mts +4 -4
  52. package/dist/{index.default-Bkg77Pxn.mjs → index.default-n4qKN7EL.mjs} +1 -1
  53. package/dist/index.mjs +5 -5
  54. package/dist/index.node.d.mts +4 -4
  55. package/dist/index.node.mjs +3 -3
  56. package/dist/inspect/app.js +31 -31
  57. package/dist/inspect/engine.js +5 -5
  58. package/dist/internal-CvsrprnU-CRY89I-b.mjs +203 -0
  59. package/dist/{local-Cr_dSjvs.mjs → local-DhkrAqPP.mjs} +1 -1
  60. package/dist/mentions-CB6ShY4R.mjs +72 -0
  61. package/dist/{targets-yM5JBUgr.mjs → middleware-CqaCwucP.mjs} +1 -33
  62. package/dist/platform-B4TnJtWu-CwcTyKg2.mjs +32 -0
  63. package/dist/{project-CFiArraX.mjs → project-DMDST9kl.mjs} +1 -1
  64. package/dist/{provider-BGjz-mDa.d.mts → provider-B0b9Qxw4.d.mts} +170 -40
  65. package/dist/provider-B3DpYSDq.mjs +220 -0
  66. package/dist/redis-tables-DN9PfIDi.mjs +3897 -0
  67. package/dist/reducer-CYPHsSmd-BQIfozGi.d.mts +170 -0
  68. package/dist/reducer-CjmvZSAb-D_OSISmj.mjs +74 -0
  69. package/dist/{contract-CuJmlrhw.mjs → reducers-BFmIXilu.mjs} +260 -287
  70. package/dist/retryable-lazy-DZWmHpii-Bye-W8q7.mjs +19 -0
  71. package/dist/scheduler-task-X1SToDjP-BVoWuInI.mjs +69 -0
  72. package/dist/{serve-BsOOc-6_.mjs → serve-CNYvA4pT.mjs} +5 -5
  73. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  74. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  75. package/dist/{server-jwPY7_wi.mjs → server-snYFkITb.mjs} +4 -4
  76. package/dist/{serving-BNbzc96L.mjs → serving-CNkgUPW1.mjs} +1 -1
  77. package/dist/session-clear-wHd5GCpZ-BnKSxReK.mjs +51 -0
  78. package/dist/{sql-DbiEnqM0.mjs → sql-U9WTxrwJ.mjs} +2 -2
  79. package/dist/{sqlite-db-Cbj03vmp.mjs → sqlite-db-Ds4majtT.mjs} +1 -1
  80. package/dist/store-D0MhBvg6-4Z2L0Kqd.d.mts +530 -0
  81. package/dist/store-N8PXxDAS-BWBgzBHp.mjs +5 -0
  82. package/dist/store-codec-DwrWlgQx-CYxGW0aD.mjs +61 -0
  83. package/dist/store-memory-Bas9Uq5C.mjs +872 -0
  84. package/dist/store-memory-ZLiZbH3R.mjs +2 -0
  85. package/dist/store-polling-BZt2Rf3D-BiEM6_HB.mjs +148 -0
  86. package/dist/store-presence-polling-JuAy2Ck2-CYpuK75X.mjs +109 -0
  87. package/dist/store-sqlite-BmwL3pUR.mjs +2 -0
  88. package/dist/store-sqlite-CxxPCcCT.mjs +1053 -0
  89. package/dist/stream-buffer-DrTyiIuo-BKy-vI14.mjs +53 -0
  90. package/dist/sync-github.d.mts +24 -17
  91. package/dist/sync-github.mjs +89 -22
  92. package/dist/sync-kapso.d.mts +15 -13
  93. package/dist/sync-kapso.mjs +17 -14
  94. package/dist/sync-slack.d.mts +12 -17
  95. package/dist/sync-slack.mjs +54 -42
  96. package/dist/sync.chat.d.mts +1 -1
  97. package/dist/sync.chat.mjs +1 -1
  98. package/dist/sync.d.mts +3 -3
  99. package/dist/sync.mjs +5 -4
  100. package/dist/targets-C-wdR5wP.mjs +33 -0
  101. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  102. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  103. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  104. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-DojIuoFg.mjs} +233 -28
  105. package/docs/01-board/01-mounting.md +1 -1
  106. package/docs/02-sync/01-overview.md +69 -47
  107. package/docs/02-sync/02-functions.md +395 -0
  108. package/docs/02-sync/03-slack.md +30 -25
  109. package/docs/02-sync/04-github.md +62 -26
  110. package/docs/02-sync/05-whatsapp.md +23 -19
  111. package/docs/02-sync/06-providers.md +58 -26
  112. package/docs/03-guides/04-triage.md +1 -1
  113. package/docs/04-reference/01-cli.md +26 -6
  114. package/docs/04-reference/02-board.md +18 -5
  115. package/docs/04-reference/03-http.md +5 -2
  116. package/docs/04-reference/05-conformance.md +1 -1
  117. package/docs/index.md +5 -10
  118. package/package.json +22 -6
  119. package/dist/a2-D24cvwpN.d.mts +0 -3359
  120. package/dist/access-CJU2-vaa.mjs +0 -226
  121. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  122. package/dist/exports-DLiE7fLY.d.mts +0 -4
  123. package/dist/provider-CuewcDNZ.mjs +0 -205
  124. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  125. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  126. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  127. package/docs/02-sync/02-callbacks.md +0 -265
@@ -0,0 +1,53 @@
1
+ import { i as PRESENCE_TIMINGS } from "./internal-CvsrprnU-CRY89I-b.mjs";
2
+ //#region ../../node_modules/.pnpm/experimental-a2@0.22.0_@upstash+qstash@2.12.0_@vercel+queue@0.7.0_ai@7.0.130_zod@4.6.5__52060f00b21aecfab049c27fa3abd2f9/node_modules/experimental-a2/dist/stream-buffer-DrTyiIuo.js
3
+ const key = ({ participant, field }) => JSON.stringify([participant, field]);
4
+ function presenceTombstones() {
5
+ const stamps = /* @__PURE__ */ new Map();
6
+ const prune = () => {
7
+ const cutoff = Date.now() - PRESENCE_TIMINGS.tombstoneMs;
8
+ for (const [id, stamp] of stamps) {
9
+ if (stamp.keptAtMs > cutoff) return;
10
+ stamps.delete(id);
11
+ }
12
+ };
13
+ return {
14
+ outranks(write) {
15
+ if (stamps.size === 0) return false;
16
+ prune();
17
+ const stamp = stamps.get(key(write));
18
+ return stamp !== void 0 && stamp.atMs > write.at.getTime();
19
+ },
20
+ keep(deletion) {
21
+ prune();
22
+ const id = key(deletion);
23
+ stamps.delete(id);
24
+ stamps.set(id, {
25
+ atMs: deletion.at.getTime(),
26
+ keptAtMs: Date.now()
27
+ });
28
+ },
29
+ drop(field) {
30
+ if (stamps.size > 0) stamps.delete(key(field));
31
+ }
32
+ };
33
+ }
34
+ const isSerializedEvent = (event) => "payloadJson" in event;
35
+ const parseSerializedEvent = (event) => ({
36
+ id: event.id,
37
+ type: event.type,
38
+ payload: JSON.parse(event.payloadJson),
39
+ index: event.index,
40
+ sessionId: event.sessionId,
41
+ createdAt: new Date(event.createdAtMs)
42
+ });
43
+ /**
44
+ * The next event a stream iterator already holds, removed without
45
+ * awaiting its `next()`, which then continues after it; `undefined` when
46
+ * it holds none. Call only while no `next()` is pending. The SSE encoder
47
+ * writes these with the event `next()` just returned, so a buffered
48
+ * backlog leaves in few writes.
49
+ */
50
+ const bufferedEvents = /* @__PURE__ */ new WeakMap();
51
+ const takeBuffered = (iterator) => bufferedEvents.get(iterator)?.();
52
+ //#endregion
53
+ export { takeBuffered as a, presenceTombstones as i, isSerializedEvent as n, parseSerializedEvent as r, bufferedEvents as t };
@@ -1,5 +1,5 @@
1
- import { d as ResolvedTarget, g as UserTokens, i as ProviderDelivery, m as SyncHandler, p as SyncContext, s as ProviderPost, t as EventMapping, u as RenderInput, v as withUserTokens } from "./provider-BGjz-mDa.mjs";
2
- import { n as ChatSyncProvider } from "./chat-provider-XCnQlxq6.mjs";
1
+ import { S as SyncUnit, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-B0b9Qxw4.mjs";
2
+ import { n as ChatSyncProvider } from "./chat-provider-DNJQyajS.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/github.d.ts
@@ -314,6 +314,7 @@ export declare const GitHubEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
314
314
  owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
315
315
  reply: z.ZodNullable<z.ZodString>;
316
316
  createRoot: z.ZodBoolean;
317
+ retitled: z.ZodOptional<z.ZodBoolean>;
317
318
  author: z.ZodObject<{
318
319
  id: z.ZodString;
319
320
  handle: z.ZodString;
@@ -324,7 +325,7 @@ export declare const GitHubEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
324
325
  id: z.ZodString;
325
326
  key: z.ZodString;
326
327
  platform: z.ZodLiteral<"github">;
327
- type: z.ZodLiteral<"pr.assigned" | "pr.closed" | "pr.draft" | "pr.labeled" | "pr.opened" | "pr.ready" | "pr.reopened" | "pr.unassigned" | "pr.unlabeled" | "review.dismissed" | "review.submitted">;
328
+ type: z.ZodLiteral<"pr.assigned" | "pr.closed" | "pr.draft" | "pr.edited" | "pr.labeled" | "pr.opened" | "pr.ready" | "pr.reopened" | "pr.unassigned" | "pr.unlabeled" | "review.dismissed" | "review.submitted">;
328
329
  subject: z.ZodObject<{
329
330
  repo: z.ZodString;
330
331
  number: z.ZodNumber;
@@ -373,6 +374,7 @@ export declare const GitHubEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
373
374
  owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
374
375
  reply: z.ZodNullable<z.ZodString>;
375
376
  createRoot: z.ZodBoolean;
377
+ retitled: z.ZodOptional<z.ZodBoolean>;
376
378
  author: z.ZodObject<{
377
379
  id: z.ZodString;
378
380
  handle: z.ZodString;
@@ -383,7 +385,7 @@ export declare const GitHubEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
383
385
  id: z.ZodString;
384
386
  key: z.ZodString;
385
387
  platform: z.ZodLiteral<"github">;
386
- type: z.ZodLiteral<"issue.assigned" | "issue.closed" | "issue.labeled" | "issue.opened" | "issue.reopened" | "issue.unassigned" | "issue.unlabeled">;
388
+ type: z.ZodLiteral<"issue.assigned" | "issue.closed" | "issue.edited" | "issue.labeled" | "issue.opened" | "issue.reopened" | "issue.unassigned" | "issue.unlabeled">;
387
389
  subject: z.ZodObject<{
388
390
  repo: z.ZodString;
389
391
  number: z.ZodNumber;
@@ -422,10 +424,9 @@ export declare const GitHubEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
422
424
  }, z.core.$loose>;
423
425
  }, z.core.$strip>)[]], "type">;
424
426
  export type GitHubEvent = z.infer<typeof GitHubEvent>;
425
- export type GitHubHandler = SyncHandler<GitHubEvent>;
426
- export type GitHubRepo = EventMapping<GitHubEvent> | {
427
- pulls?: EventMapping<GitHubEvent>;
428
- issues?: EventMapping<GitHubEvent>;
427
+ export type GitHubRepo = SyncUnit<GitHubEvent> | {
428
+ pulls?: SyncUnit<GitHubEvent>;
429
+ issues?: SyncUnit<GitHubEvent>;
429
430
  };
430
431
  export type GitHubOptions = {
431
432
  /**
@@ -439,19 +440,25 @@ export type GitHubOptions = {
439
440
  * board looks it up from the installation token; set it to skip that call.
440
441
  */
441
442
  botUserId?: number;
442
- render?: (input: RenderInput) => string;
443
- repos: Record<string, GitHubRepo>;
444
443
  };
445
444
  /**
446
- * Syncs GitHub repos with board channels. Keys are repos (`'owner/repo'`);
447
- * values are a board channel (PRs and issues become threads there), a
448
- * function, or `{ pulls, issues }` to send each somewhere else. Roots never
449
- * flow into GitHub: a board thread doesn't open an issue.
445
+ * Syncs GitHub with the board: `app` is a Vercel Connect connector
446
+ * (`'github/acme'`) or the options, and `sync` is what each event does, both
447
+ * ways, like `repos({ 'acme/www': 'prs' })`. A board thread never opens an
448
+ * issue: GitHub threads start on GitHub.
450
449
  */
451
- export declare function github(options: GitHubOptions): GitHubProvider;
450
+ export declare function github(app: string | GitHubOptions, sync: SyncFn<GitHubEvent>): GitHubProvider;
451
+ /**
452
+ * Routes each event by its repo. Keys are repos (`'owner/repo'`); values are a board channel
453
+ * that PRs and issues are imported `into`, a sync function, or `{ pulls, issues }` to route
454
+ * each its own way. A string imports and sends nothing back: what's said on the board stays
455
+ * there unless a function delivers it. A thread the board syncs in another repo keeps
456
+ * importing.
457
+ */
458
+ export declare function repos(map: Record<string, GitHubRepo>): SyncFn<GitHubEvent>;
452
459
  export declare class GitHubProvider extends ChatSyncProvider<GitHubEvent> {
453
460
  private readonly body;
454
- constructor(given: GitHubOptions);
461
+ constructor(given: GitHubOptions, sync: SyncFn<GitHubEvent>);
455
462
  whoAmI(token: string): Promise<{
456
463
  id: string;
457
464
  handle: string;
@@ -467,7 +474,7 @@ export declare class GitHubProvider extends ChatSyncProvider<GitHubEvent> {
467
474
  * (`root:<event>` or `lifecycle:<event>`), so its reactions go on the PR or issue.
468
475
  */
469
476
  react(thread: string, message: string, emoji: string, added: boolean): Promise<void>;
470
- event(context: Pick<SyncContext, 'id' | 'key' | 'event' | 'raw'>): GitHubEvent;
477
+ event(context: SyncContext): GitHubEvent;
471
478
  }
472
479
  //#endregion
473
480
  export { type UserTokens, withUserTokens };
@@ -1,6 +1,7 @@
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";
1
+ import { c as into, d as withRoutes } from "./provider-B3DpYSDq.mjs";
2
+ import { i as eventSchema, r as envelope } from "./events-CewD4kw7.mjs";
3
+ import { r as withUserTokens, t as connectUserTokens } from "./users-yryHKFMo.mjs";
4
+ import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-YHy-WbrB.mjs";
4
5
  import { z } from "zod";
5
6
  import { connectGitHubAdapter } from "@vercel/connect/chat";
6
7
  import { createGitHubAdapter } from "@chat-adapter/github";
@@ -85,6 +86,20 @@ function lifecycle(name, body) {
85
86
  createRoot: true
86
87
  }
87
88
  };
89
+ case "edited":
90
+ if (!body.changes?.title) return null;
91
+ return {
92
+ number: pr.number,
93
+ user: sender ?? {},
94
+ lifecycle: {
95
+ ...base,
96
+ type: "pr.edited",
97
+ labels: {},
98
+ reply: null,
99
+ createRoot: false,
100
+ retitled: true
101
+ }
102
+ };
88
103
  case "ready_for_review": return {
89
104
  number: pr.number,
90
105
  user: sender ?? {},
@@ -207,6 +222,18 @@ function lifecycle(name, body) {
207
222
  createRoot: true
208
223
  }
209
224
  };
225
+ if (action === "edited" && body.changes?.title) return {
226
+ number: issue.number,
227
+ user: sender ?? {},
228
+ lifecycle: {
229
+ ...base,
230
+ type: "issue.edited",
231
+ labels: {},
232
+ reply: null,
233
+ createRoot: false,
234
+ retitled: true
235
+ }
236
+ };
210
237
  if (action === "closed") return {
211
238
  number: issue.number,
212
239
  user: sender ?? {},
@@ -257,6 +284,7 @@ const GitHubPayload = z.looseObject({
257
284
  });
258
285
  const prTypes = [
259
286
  "pr.opened",
287
+ "pr.edited",
260
288
  "pr.ready",
261
289
  "pr.draft",
262
290
  "pr.reopened",
@@ -270,6 +298,7 @@ const prTypes = [
270
298
  ];
271
299
  const issueTypes = [
272
300
  "issue.opened",
301
+ "issue.edited",
273
302
  "issue.closed",
274
303
  "issue.reopened",
275
304
  "issue.labeled",
@@ -330,13 +359,39 @@ async function asPerson(token, method, path, body) {
330
359
  return response.status === 204 ? null : response.json();
331
360
  }
332
361
  /**
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.
362
+ * Syncs GitHub with the board: `app` is a Vercel Connect connector
363
+ * (`'github/acme'`) or the options, and `sync` is what each event does, both
364
+ * ways, like `repos({ 'acme/www': 'prs' })`. A board thread never opens an
365
+ * issue: GitHub threads start on GitHub.
366
+ */
367
+ function github(app, sync) {
368
+ return new GitHubProvider(typeof app === "string" ? { adapter: app } : app, sync);
369
+ }
370
+ /**
371
+ * Routes each event by its repo. Keys are repos (`'owner/repo'`); values are a board channel
372
+ * that PRs and issues are imported `into`, a sync function, or `{ pulls, issues }` to route
373
+ * each its own way. A string imports and sends nothing back: what's said on the board stays
374
+ * there unless a function delivers it. A thread the board syncs in another repo keeps
375
+ * importing.
337
376
  */
338
- function github(options) {
339
- return new GitHubProvider(options);
377
+ function repos(map) {
378
+ const unit = (value) => typeof value === "string" ? into(value) : value;
379
+ const routes = /* @__PURE__ */ new Map();
380
+ for (const [repo, value] of Object.entries(map)) {
381
+ const { pulls, issues } = typeof value === "object" ? value : {
382
+ pulls: value,
383
+ issues: value
384
+ };
385
+ const [forPulls, forIssues] = [unit(pulls), unit(issues)];
386
+ if (forPulls) routes.set(`${repo}:pulls`, forPulls);
387
+ if (forIssues) routes.set(`${repo}:issues`, forIssues);
388
+ }
389
+ const fn = async (sync) => {
390
+ const route = sync.channel === null ? void 0 : routes.get(sync.channel);
391
+ if (route) await route(sync);
392
+ else if (sync.direction === "in") await sync.import();
393
+ };
394
+ return withRoutes(fn, async (channel) => routes.has(channel));
340
395
  }
341
396
  /**
342
397
  * A connector's GitHub adapter, finding its own bot user ID. Chat SDK skips detection
@@ -368,13 +423,12 @@ function fromConnector(connector, botUserId) {
368
423
  }
369
424
  var GitHubProvider = class extends ChatSyncProvider {
370
425
  body;
371
- constructor(given) {
426
+ constructor(given, sync) {
372
427
  const options = {
373
428
  ...given,
374
429
  adapter: typeof given.adapter === "string" ? fromConnector(given.adapter, given.botUserId) : given.adapter
375
430
  };
376
431
  super({
377
- render: options.render,
378
432
  platform: "github",
379
433
  adapter: options.adapter,
380
434
  canStartThreads: false,
@@ -383,6 +437,25 @@ var GitHubProvider = class extends ChatSyncProvider {
383
437
  groupThread: (thread) => thread.replace(/:rc:[^:]+$/, ""),
384
438
  channelOf,
385
439
  isRoot: () => false,
440
+ locate(url) {
441
+ const match = /^https:\/\/github\.com\/([^/\s]+)\/([^/\s]+)\/(pull|issues)\/(\d+)[^#\s]*(?:#(issuecomment-|discussion_r)(\d+))?$/.exec(url.trim());
442
+ if (!match) return null;
443
+ const [, owner, repo, kind, number, anchor, id] = match;
444
+ const thread = `${owner}/${repo}:${kind === "issues" ? "issue:" : ""}${number}`;
445
+ if (anchor === "discussion_r" && kind !== "pull") return null;
446
+ return {
447
+ thread,
448
+ message: anchor ? id : null
449
+ };
450
+ },
451
+ async url(thread, message) {
452
+ const match = /^([^/]+)\/([^:]+):(issue:)?(\d+)(:rc:\d+)?$/.exec(thread);
453
+ if (!match) return null;
454
+ const [, owner, repo, issue, number, review] = match;
455
+ const page = `https://github.com/${owner}/${repo}/${issue ? "issues" : "pull"}/${number}`;
456
+ if (!/^\d+$/.test(message)) return page;
457
+ return `${page}#${review ? "discussion_r" : "issuecomment-"}${message}`;
458
+ },
386
459
  async fetchRoot(thread) {
387
460
  const match = /^([^/]+)\/([^:]+):(issue:)?(\d+)$/.exec(thread);
388
461
  if (!match) throw new Error(`invalid GitHub thread: ${thread}`);
@@ -497,7 +570,9 @@ var GitHubProvider = class extends ChatSyncProvider {
497
570
  }];
498
571
  },
499
572
  async resolveTarget(target) {
500
- if (!/^[^/]+\/[^:]+(?::(?:issue:)?\d+(?::rc:[^:]+)?)?$/.test(target)) throw new Error(`invalid GitHub target: ${target}`);
573
+ const review = /^([^/]+\/[^:]+:\d+):rc:[^:]+$/.exec(target);
574
+ if (review) throw new Error(`a review thread can't be a target; sync its PR, ${review[1]}`);
575
+ if (!/^[^/]+\/[^:]+(?::(?:issue:)?\d+)?$/.test(target)) throw new Error(`invalid GitHub target: ${target}`);
501
576
  const at = target.indexOf(":");
502
577
  return {
503
578
  target,
@@ -505,7 +580,7 @@ var GitHubProvider = class extends ChatSyncProvider {
505
580
  kind: at === -1 ? "channel" : "thread"
506
581
  };
507
582
  }
508
- });
583
+ }, sync);
509
584
  const converter = options.adapter.formatConverter;
510
585
  this.body = (post) => {
511
586
  try {
@@ -514,14 +589,6 @@ var GitHubProvider = class extends ChatSyncProvider {
514
589
  return post.markdown;
515
590
  }
516
591
  };
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
592
  }
526
593
  async whoAmI(token) {
527
594
  const user = await asPerson(token, "GET", "/user");
@@ -600,4 +667,4 @@ var GitHubProvider = class extends ChatSyncProvider {
600
667
  }
601
668
  };
602
669
  //#endregion
603
- export { GitHubEvent, GitHubPayload, GitHubProvider, github, withUserTokens };
670
+ export { GitHubEvent, GitHubPayload, GitHubProvider, github, repos, withUserTokens };
@@ -1,5 +1,5 @@
1
- import { m as SyncHandler, p as SyncContext, t as EventMapping, u as RenderInput } from "./provider-BGjz-mDa.mjs";
2
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-XCnQlxq6.mjs";
1
+ import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-B0b9Qxw4.mjs";
2
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-DNJQyajS.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/kapso.d.ts
@@ -132,6 +132,7 @@ export declare const WhatsAppEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
132
132
  owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
133
133
  reply: z.ZodNullable<z.ZodString>;
134
134
  createRoot: z.ZodBoolean;
135
+ retitled: z.ZodOptional<z.ZodBoolean>;
135
136
  author: z.ZodObject<{
136
137
  id: z.ZodString;
137
138
  handle: z.ZodString;
@@ -152,24 +153,25 @@ type MarkRead = (input: {
152
153
  type: 'text';
153
154
  };
154
155
  }) => Promise<unknown>;
155
- export type KapsoHandler = SyncHandler<WhatsAppEvent>;
156
156
  export type KapsoOptions = {
157
157
  adapter: Adapter;
158
- render?: (input: RenderInput) => string;
159
158
  markRead: MarkRead;
160
- numbers: Record<string, EventMapping<WhatsAppEvent>>;
161
159
  };
162
160
  /**
163
- * Syncs WhatsApp numbers on Kapso with board channels. Keys are phone number
164
- * IDs (`'16315558151'`); values are a board channel (a two-way mirror) or a
165
- * callback that handles every incoming event. Each Kapso conversation is a
166
- * thread. WhatsApp has no threads in a number, so a board thread never starts
167
- * one: a board root in a mirrored channel stays on the board.
161
+ * Syncs WhatsApp numbers on Kapso with the board: `sync` is what each event
162
+ * does, both ways, like `numbers({ '16315558151': 'support' })`. Each Kapso
163
+ * conversation is a thread. WhatsApp has no threads in a number, so a board
164
+ * thread never starts one: a board root in a mirrored channel stays on the board.
168
165
  */
169
- export declare function kapso(options: KapsoOptions): KapsoProvider;
166
+ export declare function kapso(options: KapsoOptions, sync: SyncFn<WhatsAppEvent>): KapsoProvider;
167
+ /**
168
+ * Routes each event by its WhatsApp number. Keys are phone number IDs (`'16315558151'`), not
169
+ * the numbers; values are a board channel to `mirror`, or a sync function.
170
+ */
171
+ export declare const numbers: (map: Record<string, SyncUnit<WhatsAppEvent>>) => SyncFn<WhatsAppEvent>;
170
172
  export declare class KapsoProvider extends ChatSyncProvider<WhatsAppEvent> {
171
- constructor(options: KapsoOptions);
173
+ constructor(options: KapsoOptions, sync: SyncFn<WhatsAppEvent>);
172
174
  eventId(key: string, input: ChatInput): string;
173
- event(context: Pick<SyncContext, 'id' | 'key' | 'event' | 'raw'>): WhatsAppEvent;
175
+ event(context: SyncContext): WhatsAppEvent;
174
176
  }
175
177
  //#endregion
@@ -1,27 +1,31 @@
1
- import { c as eventSchema, s as envelope } from "./provider-CuewcDNZ.mjs";
2
- import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-jFSeyNmB.mjs";
1
+ import { s as channels } from "./provider-B3DpYSDq.mjs";
2
+ import { i as eventSchema, r as envelope } from "./events-CewD4kw7.mjs";
3
+ import { r as markdownOf, t as ChatSyncProvider } from "./chat-provider-YHy-WbrB.mjs";
3
4
  import { z } from "zod";
4
5
  //#region src/sync/kapso.ts
5
6
  const decode = (id) => id.split(":").map((part) => Buffer.from(part, "base64url").toString("utf8")).join(":");
6
7
  const encode = (id) => id.split(":").map((part) => Buffer.from(part, "utf8").toString("base64url")).join(":");
7
8
  const WhatsAppEvent = eventSchema("whatsapp", "message");
8
9
  /**
9
- * Syncs WhatsApp numbers on Kapso with board channels. Keys are phone number
10
- * IDs (`'16315558151'`); values are a board channel (a two-way mirror) or a
11
- * callback that handles every incoming event. Each Kapso conversation is a
12
- * thread. WhatsApp has no threads in a number, so a board thread never starts
13
- * one: a board root in a mirrored channel stays on the board.
10
+ * Syncs WhatsApp numbers on Kapso with the board: `sync` is what each event
11
+ * does, both ways, like `numbers({ '16315558151': 'support' })`. Each Kapso
12
+ * conversation is a thread. WhatsApp has no threads in a number, so a board
13
+ * thread never starts one: a board root in a mirrored channel stays on the board.
14
14
  */
15
- function kapso(options) {
16
- return new KapsoProvider(options);
15
+ function kapso(options, sync) {
16
+ return new KapsoProvider(options, sync);
17
17
  }
18
+ /**
19
+ * Routes each event by its WhatsApp number. Keys are phone number IDs (`'16315558151'`), not
20
+ * the numbers; values are a board channel to `mirror`, or a sync function.
21
+ */
22
+ const numbers = (map) => channels(map);
18
23
  var KapsoProvider = class extends ChatSyncProvider {
19
- constructor(options) {
24
+ constructor(options, sync) {
20
25
  if (typeof options.markRead !== "function") throw new Error("kapso requires a public markRead operation for cross-instance read and typing");
21
26
  const fileIds = (raw) => (raw?.responses ?? []).flatMap((response) => response.messages?.[0]?.id ?? []);
22
27
  super({
23
28
  fileIds,
24
- render: options.render,
25
29
  platform: "whatsapp",
26
30
  adapter: options.adapter,
27
31
  canStartThreads: false,
@@ -69,8 +73,7 @@ var KapsoProvider = class extends ChatSyncProvider {
69
73
  kind: parts.length === 1 ? "channel" : "thread"
70
74
  };
71
75
  }
72
- });
73
- for (const [key, value] of Object.entries(options.numbers)) this.map(key, value);
76
+ }, sync);
74
77
  }
75
78
  eventId(key, input) {
76
79
  if (input.kind !== "reaction") return super.eventId(key, input);
@@ -83,4 +86,4 @@ var KapsoProvider = class extends ChatSyncProvider {
83
86
  }
84
87
  };
85
88
  //#endregion
86
- export { KapsoProvider, WhatsAppEvent, kapso };
89
+ export { KapsoProvider, WhatsAppEvent, kapso, numbers };
@@ -1,6 +1,6 @@
1
- import { v as ExternalLink } from "./reducers-B_vUGQRp.mjs";
2
- import { d as ResolvedTarget, g as UserTokens, i as ProviderDelivery, m as SyncHandler, p as SyncContext, s as ProviderPost, t as EventMapping, u as RenderInput, v as withUserTokens } from "./provider-BGjz-mDa.mjs";
3
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-XCnQlxq6.mjs";
1
+ import { f as ExternalLink } from "./folds-CRYY14kc.mjs";
2
+ import { C as channels, _ as withUserTokens, d as ResolvedTarget, f as SyncContext, h as UserTokens, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-B0b9Qxw4.mjs";
3
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-DNJQyajS.mjs";
4
4
  import { z } from "zod";
5
5
  import { Adapter } from "chat";
6
6
  //#region src/sync/slack.d.ts
@@ -26,9 +26,6 @@ export type SlackClient = {
26
26
  }): Promise<{
27
27
  channel?: SlackChannel;
28
28
  }>;
29
- join(args: {
30
- channel: string;
31
- }): Promise<unknown>;
32
29
  /** Reads a linked message or a thread; needs `channels:history` (`groups:history` for private channels). */
33
30
  replies?(args: {
34
31
  channel: string;
@@ -300,6 +297,7 @@ export declare const SlackEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
300
297
  owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
301
298
  reply: z.ZodNullable<z.ZodString>;
302
299
  createRoot: z.ZodBoolean;
300
+ retitled: z.ZodOptional<z.ZodBoolean>;
303
301
  author: z.ZodObject<{
304
302
  id: z.ZodString;
305
303
  handle: z.ZodString;
@@ -313,7 +311,6 @@ export declare const SlackEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
313
311
  platform: z.ZodLiteral<"slack">;
314
312
  }, z.core.$strip>], "kind">;
315
313
  export type SlackEvent = z.infer<typeof SlackEvent>;
316
- export type SlackHandler = SyncHandler<SlackEvent>;
317
314
  export type SlackOptions = {
318
315
  /**
319
316
  * A Vercel Connect connector (`'slack/acme'`): board builds the Slack adapter,
@@ -321,22 +318,20 @@ export type SlackOptions = {
321
318
  * Or your own Chat SDK adapter, wrapped in `withUserTokens` to post as people.
322
319
  */
323
320
  adapter: Adapter | string;
324
- render?: (input: RenderInput) => string;
325
321
  /** Not needed with a connector. */
326
322
  client?: () => Promise<SlackClient>;
327
- channels: Record<string, EventMapping<SlackEvent>>;
328
323
  };
329
324
  /**
330
- * Syncs Slack channels with board channels. Keys are channels by name
331
- * (`'#prs'`) or ID (`'C0123ABC'`); values are a board channel (a two-way
332
- * mirror) or a callback that handles every incoming event. The app joins public
333
- * channels it isn't in; private ones need an invite.
325
+ * Syncs a Slack workspace with the board: `app` is a Vercel Connect connector
326
+ * (`'slack/acme'`) or the options, and `sync` is what each event does, both ways,
327
+ * like `channels({ '#prs': 'prs' })`. Invite the app to each channel it syncs:
328
+ * it never joins one on its own.
334
329
  */
335
- export declare function slack(options: SlackOptions): SlackProvider;
330
+ export declare function slack(app: string | SlackOptions, sync: SyncFn<SlackEvent>): SlackProvider;
336
331
  export declare class SlackProvider extends ChatSyncProvider<SlackEvent> {
337
332
  private readonly web;
338
333
  private customize;
339
- constructor(given: SlackOptions);
334
+ constructor(given: SlackOptions, sync: SyncFn<SlackEvent>);
340
335
  /**
341
336
  * An agent's message goes out under its own name (`clever-lemur via vboard-factory`), and its
342
337
  * picture when it has one Slack can show, instead of the app's, with the text's
@@ -358,7 +353,7 @@ export declare class SlackProvider extends ChatSyncProvider<SlackEvent> {
358
353
  */
359
354
  permalink(event: SlackEvent): Promise<string | null>;
360
355
  eventId(key: string, input: ChatInput): string;
361
- event(context: Pick<SyncContext, 'id' | 'key' | 'event' | 'raw'>): SlackEvent;
356
+ event(context: SyncContext): SlackEvent;
362
357
  }
363
358
  //#endregion
364
- export { type UserTokens, withUserTokens };
359
+ export { type UserTokens, channels, withUserTokens };