@vercel/board 0.16.0 → 0.18.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 (130) hide show
  1. package/README.md +27 -27
  2. package/dist/{a2-C41hD8SF.mjs → a2-ChRLRupa.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-CMlL3yq3.mjs} +2 -2
  31. package/dist/{backend-http-BVRno-ca.mjs → backend-http-Ddzd5kTS.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-CgjfMrQ-.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-C5Cpz3sa.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-BGYw0uNL.mjs} +5 -5
  73. package/dist/{server-jwPY7_wi.mjs → server-DPxBSogx.mjs} +4 -4
  74. package/dist/server-DtFrWxi3-Boa6gdWO.mjs +3602 -0
  75. package/dist/server-qgez0Xny-D5mLfqFQ.d.mts +385 -0
  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-linear.d.mts +219 -0
  95. package/dist/sync-linear.mjs +311 -0
  96. package/dist/sync-slack.d.mts +12 -17
  97. package/dist/sync-slack.mjs +54 -42
  98. package/dist/sync.chat.d.mts +1 -1
  99. package/dist/sync.chat.mjs +1 -1
  100. package/dist/sync.d.mts +3 -3
  101. package/dist/sync.mjs +5 -4
  102. package/dist/targets-C-wdR5wP.mjs +33 -0
  103. package/dist/{tokens-DKLn5P9B.mjs → tokens-DhuzKzAl.mjs} +1 -1
  104. package/dist/{types-BHCjTZdB.d.mts → types-8yq2VnKI.d.mts} +2 -2
  105. package/dist/{users-Dls7_ANV.mjs → users-yryHKFMo.mjs} +2 -2
  106. package/dist/{watch-spec-W0c7ypHJ.mjs → watchers-hash-RLtnSF-t.mjs} +233 -28
  107. package/docs/01-board/01-mounting.md +1 -1
  108. package/docs/02-sync/01-overview.md +72 -48
  109. package/docs/02-sync/02-functions.md +398 -0
  110. package/docs/02-sync/03-slack.md +30 -25
  111. package/docs/02-sync/04-github.md +62 -26
  112. package/docs/02-sync/05-whatsapp.md +23 -19
  113. package/docs/02-sync/06-providers.md +60 -27
  114. package/docs/02-sync/07-linear.md +129 -0
  115. package/docs/03-guides/04-triage.md +1 -1
  116. package/docs/04-reference/01-cli.md +26 -6
  117. package/docs/04-reference/02-board.md +18 -5
  118. package/docs/04-reference/03-http.md +5 -2
  119. package/docs/04-reference/05-conformance.md +1 -1
  120. package/docs/index.md +5 -10
  121. package/package.json +31 -6
  122. package/dist/a2-D24cvwpN.d.mts +0 -3359
  123. package/dist/access-CJU2-vaa.mjs +0 -226
  124. package/dist/attempts-Bxp9KgPf.mjs +0 -919
  125. package/dist/exports-DLiE7fLY.d.mts +0 -4
  126. package/dist/provider-CuewcDNZ.mjs +0 -205
  127. package/dist/redis-tables-B35vv4CZ.mjs +0 -842
  128. package/dist/reducers-B_vUGQRp.d.mts +0 -1172
  129. package/dist/watchers-hash-CLjsa6od.mjs +0 -225
  130. package/docs/02-sync/02-callbacks.md +0 -265
@@ -0,0 +1,219 @@
1
+ import { S as SyncUnit, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, o as ProviderPost, r as ProviderDelivery, y as SyncFn } from "./provider-B0b9Qxw4.mjs";
2
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-DNJQyajS.mjs";
3
+ import { z } from "zod";
4
+ import { Adapter } from "chat";
5
+ //#region src/sync/linear.d.ts
6
+ export declare const LinearEvent: z.ZodDiscriminatedUnion<[z.ZodObject<{
7
+ channel: z.ZodString;
8
+ thread: z.ZodString;
9
+ subthread: z.ZodNullable<z.ZodString>;
10
+ kind: z.ZodLiteral<"message">;
11
+ message: z.ZodString;
12
+ isRoot: z.ZodBoolean;
13
+ text: z.ZodString;
14
+ context: z.ZodNullable<z.ZodString>;
15
+ author: z.ZodObject<{
16
+ id: z.ZodString;
17
+ handle: z.ZodString;
18
+ name: z.ZodString;
19
+ avatar: z.ZodNullable<z.ZodString>;
20
+ isBot: z.ZodBoolean;
21
+ }, z.core.$strip>;
22
+ files: z.ZodOptional<z.ZodArray<z.ZodObject<{
23
+ type: z.ZodEnum<{
24
+ audio: "audio";
25
+ file: "file";
26
+ image: "image";
27
+ video: "video";
28
+ }>;
29
+ name: z.ZodOptional<z.ZodString>;
30
+ mimeType: z.ZodOptional<z.ZodString>;
31
+ size: z.ZodOptional<z.ZodNumber>;
32
+ url: z.ZodOptional<z.ZodString>;
33
+ fetchMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
34
+ }, z.core.$strip>>>;
35
+ links: z.ZodOptional<z.ZodArray<z.ZodObject<{
36
+ url: z.ZodString;
37
+ thread: z.ZodString;
38
+ message: z.ZodString;
39
+ snapshot: z.ZodNullable<z.ZodObject<{
40
+ author: z.ZodString;
41
+ text: z.ZodString;
42
+ createdAt: z.ZodNullable<z.ZodString>;
43
+ files: z.ZodArray<z.ZodObject<{
44
+ name: z.ZodString;
45
+ url: z.ZodNullable<z.ZodString>;
46
+ }, z.core.$strip>>;
47
+ }, z.core.$strip>>;
48
+ }, z.core.$strip>>>;
49
+ id: z.ZodString;
50
+ key: z.ZodString;
51
+ raw: z.ZodUnknown;
52
+ platform: z.ZodLiteral<"linear">;
53
+ type: z.ZodLiteral<"comment">;
54
+ }, z.core.$strip>, z.ZodObject<{
55
+ channel: z.ZodString;
56
+ thread: z.ZodString;
57
+ subthread: z.ZodNullable<z.ZodString>;
58
+ kind: z.ZodLiteral<"edited">;
59
+ message: z.ZodString;
60
+ text: z.ZodString;
61
+ author: z.ZodObject<{
62
+ id: z.ZodString;
63
+ handle: z.ZodString;
64
+ name: z.ZodString;
65
+ avatar: z.ZodNullable<z.ZodString>;
66
+ isBot: z.ZodBoolean;
67
+ }, z.core.$strip>;
68
+ links: z.ZodOptional<z.ZodArray<z.ZodObject<{
69
+ url: z.ZodString;
70
+ thread: z.ZodString;
71
+ message: z.ZodString;
72
+ snapshot: z.ZodNullable<z.ZodObject<{
73
+ author: z.ZodString;
74
+ text: z.ZodString;
75
+ createdAt: z.ZodNullable<z.ZodString>;
76
+ files: z.ZodArray<z.ZodObject<{
77
+ name: z.ZodString;
78
+ url: z.ZodNullable<z.ZodString>;
79
+ }, z.core.$strip>>;
80
+ }, z.core.$strip>>;
81
+ }, z.core.$strip>>>;
82
+ linksOnly: z.ZodOptional<z.ZodBoolean>;
83
+ id: z.ZodString;
84
+ key: z.ZodString;
85
+ raw: z.ZodUnknown;
86
+ platform: z.ZodLiteral<"linear">;
87
+ type: z.ZodLiteral<"message.edited">;
88
+ }, z.core.$strip>, z.ZodObject<{
89
+ channel: z.ZodString;
90
+ thread: z.ZodString;
91
+ subthread: z.ZodNullable<z.ZodString>;
92
+ kind: z.ZodLiteral<"deleted">;
93
+ message: z.ZodString;
94
+ id: z.ZodString;
95
+ key: z.ZodString;
96
+ raw: z.ZodUnknown;
97
+ platform: z.ZodLiteral<"linear">;
98
+ type: z.ZodLiteral<"message.deleted">;
99
+ }, z.core.$strip>, z.ZodObject<{
100
+ channel: z.ZodString;
101
+ thread: z.ZodString;
102
+ subthread: z.ZodNullable<z.ZodString>;
103
+ kind: z.ZodLiteral<"reaction">;
104
+ message: z.ZodString;
105
+ emoji: z.ZodString;
106
+ added: z.ZodBoolean;
107
+ author: z.ZodObject<{
108
+ id: z.ZodString;
109
+ handle: z.ZodString;
110
+ name: z.ZodString;
111
+ avatar: z.ZodNullable<z.ZodString>;
112
+ isBot: z.ZodBoolean;
113
+ }, z.core.$strip>;
114
+ id: z.ZodString;
115
+ key: z.ZodString;
116
+ raw: z.ZodUnknown;
117
+ platform: z.ZodLiteral<"linear">;
118
+ type: z.ZodEnum<{
119
+ "reaction.added": "reaction.added";
120
+ "reaction.removed": "reaction.removed";
121
+ }>;
122
+ }, z.core.$strip>, z.ZodObject<{
123
+ channel: z.ZodString;
124
+ thread: z.ZodString;
125
+ subthread: z.ZodNullable<z.ZodString>;
126
+ kind: z.ZodLiteral<"lifecycle">;
127
+ type: z.ZodString;
128
+ title: z.ZodString;
129
+ url: z.ZodNullable<z.ZodString>;
130
+ labels: z.ZodRecord<z.ZodString, z.ZodString>;
131
+ unset: z.ZodArray<z.ZodString>;
132
+ owns: z.ZodOptional<z.ZodArray<z.ZodString>>;
133
+ reply: z.ZodNullable<z.ZodString>;
134
+ createRoot: z.ZodBoolean;
135
+ retitled: z.ZodOptional<z.ZodBoolean>;
136
+ author: z.ZodObject<{
137
+ id: z.ZodString;
138
+ handle: z.ZodString;
139
+ name: z.ZodString;
140
+ avatar: z.ZodNullable<z.ZodString>;
141
+ isBot: z.ZodBoolean;
142
+ }, z.core.$strip>;
143
+ id: z.ZodString;
144
+ key: z.ZodString;
145
+ raw: z.ZodUnknown;
146
+ platform: z.ZodLiteral<"linear">;
147
+ }, z.core.$strip>], "kind">;
148
+ export type LinearEvent = z.infer<typeof LinearEvent>;
149
+ export type LinearOptions = {
150
+ /**
151
+ * A Vercel Connect connector (`'linear/acme'`) holding a Linear app's token: board builds the
152
+ * Chat SDK adapter from it. Or your own adapter from `createLinearAdapter`, in its default
153
+ * `comments` mode, with the app's own token: the adapter skips what its token's user writes.
154
+ */
155
+ adapter: Adapter | string;
156
+ };
157
+ /**
158
+ * Syncs Linear with the board: `app` is a Vercel Connect connector (`'linear/acme'`) or the
159
+ * options, and `sync` is what each event does, both ways, like `teams({ ENG: 'dev' })`. Each
160
+ * issue is a thread: its comments are replies, and a board root in a mirrored team opens an issue.
161
+ */
162
+ export declare function linear(app: string | LinearOptions, sync: SyncFn<LinearEvent>): LinearProvider;
163
+ /**
164
+ * Routes each event by its Linear team. Keys are team keys (`'ENG'`, the prefix of `ENG-123`);
165
+ * values are a board channel to `mirror`, or a sync function.
166
+ */
167
+ export declare const teams: (map: Record<string, SyncUnit<LinearEvent>>) => SyncFn<LinearEvent>;
168
+ export declare class LinearProvider extends ChatSyncProvider<LinearEvent> {
169
+ private readonly linear;
170
+ private readonly teamIds;
171
+ constructor(given: LinearOptions, sync: SyncFn<LinearEvent>);
172
+ /**
173
+ * Runs a Linear API call. A Connect token lives in the adapter's request context, which only a
174
+ * webhook sets up: anything else (a delivery, a root fetched on a later pull) asks for one here.
175
+ */
176
+ private api;
177
+ resolveChannel(key: string): Promise<string>;
178
+ /**
179
+ * `ENG` is a team; `ENG:<uuid>`, a bare UUID, or `ENG-12` is an issue, by its team and UUID:
180
+ * comments come in under the UUID, which stays when the issue is renamed.
181
+ */
182
+ resolveTarget(target: string): Promise<ResolvedTarget>;
183
+ fetchRoot(thread: string): Promise<{
184
+ text: string;
185
+ raw: {
186
+ issue: {
187
+ id: string;
188
+ identifier: string;
189
+ title: string;
190
+ url: string;
191
+ };
192
+ };
193
+ author: {
194
+ id: string;
195
+ handle: string;
196
+ name: string;
197
+ avatar: string | null;
198
+ isBot: boolean;
199
+ };
200
+ labels: {
201
+ issue: string;
202
+ state?: string | undefined;
203
+ };
204
+ }>;
205
+ private client;
206
+ /** A team's ID from its key, which Linear's API asks for to open an issue. */
207
+ private team;
208
+ protected normalize(key: string, input: ChatInput): Promise<ReceivedEvent>;
209
+ private teamOfIssue;
210
+ inboxChannel(key: string, thread: string): string;
211
+ /** A root in a mirrored team opens an issue, titled with its first line; a reply is a comment. */
212
+ post(target: string, kind: ResolvedTarget['kind'], post: ProviderPost): Promise<ProviderDelivery>;
213
+ edit(thread: string, message: string, post: ProviderPost): Promise<undefined>;
214
+ delete(thread: string, message: string): Promise<void>;
215
+ react(thread: string, message: string, emoji: string, added: boolean): Promise<void>;
216
+ startTyping(): Promise<void>;
217
+ event(context: SyncContext): LinearEvent;
218
+ }
219
+ //#endregion
@@ -0,0 +1,311 @@
1
+ import { i as UnreadableError, 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";
4
+ import "zod";
5
+ import { connectLinearAdapter } from "@vercel/connect/chat";
6
+ import { createLinearAdapter } from "@chat-adapter/linear";
7
+ //#region src/sync/linear.ts
8
+ const LinearEvent = eventSchema("linear", "comment");
9
+ /**
10
+ * Syncs Linear with the board: `app` is a Vercel Connect connector (`'linear/acme'`) or the
11
+ * options, and `sync` is what each event does, both ways, like `teams({ ENG: 'dev' })`. Each
12
+ * issue is a thread: its comments are replies, and a board root in a mirrored team opens an issue.
13
+ */
14
+ function linear(app, sync) {
15
+ return new LinearProvider(typeof app === "string" ? { adapter: app } : app, sync);
16
+ }
17
+ /**
18
+ * Routes each event by its Linear team. Keys are team keys (`'ENG'`, the prefix of `ENG-123`);
19
+ * values are a board channel to `mirror`, or a sync function.
20
+ */
21
+ const teams = (map) => channels(map);
22
+ /** A person's handle, the end of their profile URL (`…/profiles/ada`), as the adapter reads it. */
23
+ const handleOf = (user) => /\/profiles\/([^/?#]+)/.exec(user?.url ?? "")?.[1] ?? user?.displayName ?? user?.name;
24
+ const author = (user, isBot) => ({
25
+ id: `linear:user:${user?.id ?? "unknown"}`,
26
+ handle: handleOf(user) ?? "unknown",
27
+ name: user?.name ?? user?.displayName ?? "unknown",
28
+ avatar: user?.avatarUrl ?? null,
29
+ isBot: isBot || user?.type !== void 0 && user.type !== "user"
30
+ });
31
+ /** `ENG` from `ENG-123`, or from a Linear URL that names an issue (`…/issue/ENG-123/…`). */
32
+ const teamOf = (identifierOrUrl) => /(?:^|\/issue\/)([A-Z0-9_]+)-\d+/i.exec(identifierOrUrl ?? "")?.[1]?.toUpperCase() ?? null;
33
+ /** The copy of an issue the board opened: the issue itself, not a comment. */
34
+ const OPENED = "issue";
35
+ /** A root's copy: one the board opened, or imported (`lifecycle:<event>`, `root:<event>`). Comment IDs are UUIDs. */
36
+ const isIssue = (message) => message === OPENED || message.includes(":");
37
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
38
+ /** `ENG:<issueId>`, or `ENG:<issueId>:c:<commentId>` (a comment thread): the team, and the issue's UUID. */
39
+ function parseThread(thread) {
40
+ const match = /^([A-Z0-9_]+):([^:]+)(?::c:([^:]+))?$/i.exec(thread);
41
+ if (!match) throw new Error(`invalid Linear thread: ${thread}`);
42
+ return {
43
+ team: match[1].toUpperCase(),
44
+ issue: match[2],
45
+ comment: match[3] ?? null
46
+ };
47
+ }
48
+ /** A board message's first line, as an issue title: Linear titles are plain text. */
49
+ function titleOf(markdown) {
50
+ return (markdown.split("\n").map((text) => text.trim()).find(Boolean) ?? "").replace(/^\*\*[^*\n]+\*\*:\s*/, "").replace(/\*\*|__|`/g, "").replace(/^[#>\s]+/, "").replace(/\s+/g, " ").trim().slice(0, 255) || "Untitled";
51
+ }
52
+ function stateOf(state) {
53
+ return state?.type ?? state?.name?.toLowerCase() ?? null;
54
+ }
55
+ var LinearProvider = class extends ChatSyncProvider {
56
+ linear;
57
+ teamIds = /* @__PURE__ */ new Map();
58
+ constructor(given, sync) {
59
+ const adapter = typeof given.adapter === "string" ? createLinearAdapter({
60
+ ...connectLinearAdapter(given.adapter),
61
+ mode: "comments"
62
+ }) : given.adapter;
63
+ super({
64
+ platform: "linear",
65
+ adapter,
66
+ canStartThreads: true,
67
+ resolveChannel: (key) => Promise.resolve(key.toUpperCase()),
68
+ resolveTarget: (target) => Promise.resolve({
69
+ target,
70
+ channel: target,
71
+ kind: "channel"
72
+ }),
73
+ groupThread: (thread) => thread.replace(/:c:[^:]+$/, ""),
74
+ channelOf: (thread) => thread.slice(0, thread.indexOf(":")),
75
+ toChatId: (id) => id.slice(id.indexOf(":") + 1),
76
+ isRoot: () => false,
77
+ context: () => null,
78
+ markdown(message) {
79
+ const body = message.raw?.comment?.body;
80
+ return typeof body === "string" && body.trim() ? body.trim() : markdownOf(message);
81
+ },
82
+ async avatar(message) {
83
+ return message.raw?.comment?.user?.avatarUrl ?? null;
84
+ },
85
+ missed(_headers, body) {
86
+ let payload;
87
+ try {
88
+ payload = JSON.parse(body);
89
+ } catch {
90
+ return [];
91
+ }
92
+ if (payload.type !== "Comment" || payload.action !== "update" && payload.action !== "remove") return [];
93
+ const { data } = payload;
94
+ const issueId = data.issueId ?? data.issue?.id;
95
+ if (!issueId || !data.user) return [];
96
+ const message = adapter.parseMessage({
97
+ kind: "comment",
98
+ organizationId: payload.organizationId,
99
+ comment: {
100
+ ...data,
101
+ issueId,
102
+ parentId: data.parentId ?? void 0,
103
+ url: payload.url,
104
+ user: {
105
+ type: "user",
106
+ id: data.user.id,
107
+ displayName: handleOf(data.user) ?? "unknown",
108
+ fullName: data.user.name ?? "unknown",
109
+ email: void 0,
110
+ avatarUrl: data.user.avatarUrl ?? void 0
111
+ }
112
+ }
113
+ });
114
+ if (message.author.isMe) return [];
115
+ return payload.action === "update" ? [{
116
+ kind: "edited",
117
+ message
118
+ }] : [{
119
+ kind: "deleted",
120
+ thread: message.threadId,
121
+ message: message.id,
122
+ raw: payload
123
+ }];
124
+ },
125
+ intercept(key, headers, body) {
126
+ let payload;
127
+ try {
128
+ payload = JSON.parse(body);
129
+ } catch {
130
+ return [];
131
+ }
132
+ if (payload.type !== "Issue" || !payload.data?.identifier) return [];
133
+ const { data } = payload;
134
+ const created = payload.action === "create";
135
+ const moved = payload.action === "update" && payload.updatedFrom?.stateId !== void 0;
136
+ const retitled = payload.action === "update" && payload.updatedFrom?.title !== void 0;
137
+ if (!created && !moved && !retitled) return [];
138
+ const team = data.team?.key ?? teamOf(data.identifier);
139
+ const state = stateOf(data.state);
140
+ const own = payload.actor?.id !== void 0 && payload.actor.id === adapter.defaultBotUserId;
141
+ const delivery = headers.get("linear-delivery") ?? `${payload.webhookId}:${payload.webhookTimestamp}`;
142
+ return [{
143
+ event: {
144
+ kind: "lifecycle",
145
+ channel: `${key}:${team}`,
146
+ thread: `${key}:${team}:${data.id}`,
147
+ type: created ? "issue.created" : "issue.updated",
148
+ title: `${data.identifier}: ${data.title}`,
149
+ url: data.url ?? payload.url ?? null,
150
+ labels: {
151
+ issue: data.identifier,
152
+ ...state && (created || moved) ? { state } : {}
153
+ },
154
+ unset: [],
155
+ reply: null,
156
+ createRoot: created && !own,
157
+ ...retitled ? { retitled: true } : {},
158
+ author: author(payload.actor, own)
159
+ },
160
+ raw: payload,
161
+ dedupe: `lifecycle:${delivery}`
162
+ }];
163
+ }
164
+ }, sync);
165
+ this.linear = adapter;
166
+ }
167
+ /**
168
+ * Runs a Linear API call. A Connect token lives in the adapter's request context, which only a
169
+ * webhook sets up: anything else (a delivery, a root fetched on a later pull) asks for one here.
170
+ */
171
+ async api(fn) {
172
+ await this.ready();
173
+ return this.linear.isConnectMode() ? this.linear.withConnectClient(void 0, fn) : fn();
174
+ }
175
+ async resolveChannel(key) {
176
+ await this.team(key);
177
+ return key.toUpperCase();
178
+ }
179
+ /**
180
+ * `ENG` is a team; `ENG:<uuid>`, a bare UUID, or `ENG-12` is an issue, by its team and UUID:
181
+ * comments come in under the UUID, which stays when the issue is renamed.
182
+ */
183
+ async resolveTarget(target) {
184
+ if (/:c:[^:]+$/.test(target)) throw new Error(`a comment thread can't be a target; sync its issue, ${target.replace(/:c:[^:]+$/, "")}`);
185
+ if (/^[A-Z0-9_]+$/i.test(target)) return {
186
+ target: target.toUpperCase(),
187
+ channel: target.toUpperCase(),
188
+ kind: "channel"
189
+ };
190
+ const scoped = /^([A-Z0-9_]+):([^:]+)$/i.exec(target);
191
+ if (scoped && UUID.test(scoped[2])) return {
192
+ target: `${scoped[1].toUpperCase()}:${scoped[2]}`,
193
+ channel: scoped[1].toUpperCase(),
194
+ kind: "thread"
195
+ };
196
+ if (!UUID.test(target) && !/^[A-Z0-9_]+-\d+$/i.test(target)) throw new Error(`invalid Linear target: ${target}`);
197
+ const issue = await this.api(() => this.client().issue(target));
198
+ const team = teamOf(issue.identifier) ?? await this.api(async () => (await issue.team)?.key);
199
+ if (!team) throw new Error(`Linear didn't say which team ${target} is in`);
200
+ return {
201
+ target: `${team.toUpperCase()}:${issue.id}`,
202
+ channel: team.toUpperCase(),
203
+ kind: "thread"
204
+ };
205
+ }
206
+ async fetchRoot(thread) {
207
+ return this.api(async () => {
208
+ const issue = await this.client().issue(parseThread(thread).issue);
209
+ const [state, creator] = await Promise.all([issue.state, issue.creator]);
210
+ const found = stateOf(state);
211
+ return {
212
+ text: `${issue.identifier}: ${issue.title}\n${issue.url}`,
213
+ raw: { issue: {
214
+ id: issue.id,
215
+ identifier: issue.identifier,
216
+ title: issue.title,
217
+ url: issue.url
218
+ } },
219
+ author: author(creator, false),
220
+ labels: {
221
+ issue: issue.identifier,
222
+ ...found ? { state: found } : {}
223
+ }
224
+ };
225
+ });
226
+ }
227
+ client() {
228
+ return this.linear.linearClient;
229
+ }
230
+ /** A team's ID from its key, which Linear's API asks for to open an issue. */
231
+ async team(key) {
232
+ const wanted = key.toUpperCase();
233
+ const known = this.teamIds.get(wanted);
234
+ if (known) return known;
235
+ const { nodes } = await this.api(() => this.client().teams({ filter: { key: { eq: wanted } } }));
236
+ const team = nodes.find((node) => node.key.toUpperCase() === wanted);
237
+ if (!team) throw new UnreadableError("channel_not_found", `no Linear team ${wanted}`);
238
+ this.teamIds.set(wanted, team.id);
239
+ return team.id;
240
+ }
241
+ async normalize(key, input) {
242
+ const received = await super.normalize(key, input);
243
+ if (input.kind === "reaction") return received;
244
+ const raw = input.kind === "deleted" ? input.raw : input.message.raw;
245
+ const comment = raw?.comment ?? raw?.data;
246
+ const issue = comment?.issueId ?? raw?.data?.issue?.id;
247
+ const root = comment?.parentId || comment?.id;
248
+ if (!issue || !root) return received;
249
+ const team = teamOf(raw?.comment?.url ?? raw?.url) ?? teamOf(raw?.data?.issue?.identifier) ?? await this.teamOfIssue(issue);
250
+ const thread = `${key}:${team}:${issue}`;
251
+ const event = {
252
+ ...received.event,
253
+ channel: `${key}:${team}`,
254
+ thread,
255
+ subthread: `${thread}:c:${root}`
256
+ };
257
+ return {
258
+ ...received,
259
+ event
260
+ };
261
+ }
262
+ async teamOfIssue(id) {
263
+ return this.api(async () => {
264
+ const issue = await this.client().issue(id);
265
+ const team = teamOf(issue.identifier) ?? (await issue.team)?.key;
266
+ if (!team) throw new Error(`Linear didn't say which team issue ${id} is in`);
267
+ return team.toUpperCase();
268
+ });
269
+ }
270
+ inboxChannel(key, thread) {
271
+ return `${key}:${parseThread(thread.slice(key.length + 1)).team}`;
272
+ }
273
+ /** A root in a mirrored team opens an issue, titled with its first line; a reply is a comment. */
274
+ async post(target, kind, post) {
275
+ if (kind === "thread") return {
276
+ ...await this.api(() => super.post(target, kind, post)),
277
+ thread: target
278
+ };
279
+ const teamId = await this.team(target);
280
+ const created = await this.api(() => this.client().createIssue({
281
+ teamId,
282
+ title: titleOf(post.markdown),
283
+ description: post.markdown
284
+ }));
285
+ if (!created.issueId) throw new Error(`Linear didn't return the issue it opened in ${target}`);
286
+ return {
287
+ thread: `${target.toUpperCase()}:${created.issueId}`,
288
+ message: OPENED
289
+ };
290
+ }
291
+ async edit(thread, message, post) {
292
+ if (message === OPENED) return void await this.api(() => this.client().updateIssue(parseThread(thread).issue, { description: post.markdown }));
293
+ if (!isIssue(message)) await this.api(() => super.edit(thread, message, post));
294
+ }
295
+ async delete(thread, message) {
296
+ if (!isIssue(message)) await this.api(() => super.delete(thread, message));
297
+ }
298
+ async react(thread, message, emoji, added) {
299
+ if (added && !isIssue(message)) await this.api(() => super.react(thread, message, emoji, added));
300
+ }
301
+ async startTyping() {}
302
+ event(context) {
303
+ const base = envelope(this.platform, context.key, context.event, context.raw ?? {}, context.id);
304
+ return LinearEvent.parse({
305
+ ...base,
306
+ type: context.event.kind === "message" ? "comment" : base.type
307
+ });
308
+ }
309
+ };
310
+ //#endregion
311
+ export { LinearEvent, LinearProvider, linear, teams };
@@ -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 };