@vercel/board 0.19.0 → 0.20.1

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 (63) hide show
  1. package/dist/{a2-C1obi0Qb.mjs → a2-QtmN9EhW.mjs} +36 -6
  2. package/dist/access.mjs +3 -3
  3. package/dist/{actors-BOamkEuz.mjs → actors-rPnEQLZu.mjs} +2 -2
  4. package/dist/{auth-Drg9FiC2.mjs → auth-5nLsx9qB.mjs} +1 -1
  5. package/dist/{backend-B5GCP1oK.d.mts → backend-CWmd4UH-.d.mts} +8 -1
  6. package/dist/backend-a2.d.mts +22 -4
  7. package/dist/backend-a2.mjs +1 -1
  8. package/dist/{backend-http-Ca6b0FPA.d.mts → backend-http-BfWQfvqB.d.mts} +22 -4
  9. package/dist/{backend-http-BMVDbtD_.mjs → backend-http-CbcjkHEn.mjs} +7 -3
  10. package/dist/{backend-http-DMpQ750P.mjs → backend-http-maHy-JW3.mjs} +19 -6
  11. package/dist/backend-http.d.mts +1 -1
  12. package/dist/backend-http.mjs +1 -1
  13. package/dist/backend-http.node.d.mts +1 -1
  14. package/dist/backend-http.node.mjs +1 -1
  15. package/dist/{chat-provider-B5uCc0an.d.mts → chat-provider-D0k3y8zY.d.mts} +4 -1
  16. package/dist/{chat-provider-YHy-WbrB.mjs → chat-provider-DnHibqnM.mjs} +4 -1
  17. package/dist/cli-embedded.d.mts +47 -0
  18. package/dist/cli-embedded.mjs +64 -0
  19. package/dist/cli.mjs +73 -1661
  20. package/dist/{client-BA-J7usH.mjs → client-ClznIBMg.mjs} +2 -0
  21. package/dist/{conformance-D0Z4dXC_.mjs → conformance-DQ9qVzWP.mjs} +1 -1
  22. package/dist/conformance.d.mts +1 -1
  23. package/dist/conformance.mjs +1 -1
  24. package/dist/{engine-D0oBUBEK.mjs → engine-kHfSOXnQ.mjs} +15 -4
  25. package/dist/{errors-DX3hpwWt.mjs → errors-CQ-fwxM3.mjs} +13 -1
  26. package/dist/{exports-CUGshfRF.mjs → exports-Dd8YdgO-.mjs} +15 -6
  27. package/dist/exports-ggfE9jNv.d.mts +4 -0
  28. package/dist/index-DsMPCv_K.d.mts +11 -0
  29. package/dist/index.d.mts +4 -4
  30. package/dist/{index.default-Cm6GsLdj.mjs → index.default-BqstUHvz.mjs} +1 -1
  31. package/dist/index.mjs +6 -6
  32. package/dist/index.node.d.mts +4 -12
  33. package/dist/index.node.mjs +4 -4
  34. package/dist/inspect/app.js +93 -92
  35. package/dist/inspect/engine.js +4 -4
  36. package/dist/main-CRLa7i3I.mjs +1745 -0
  37. package/dist/{middleware-CqaCwucP.mjs → middleware-DUjyQP_a.mjs} +1 -1
  38. package/dist/{provider-B1Z3Ok86.d.mts → provider-D11Dfda0.d.mts} +86 -32
  39. package/dist/{provider-B3DpYSDq.mjs → provider-poIkXapd.mjs} +4 -0
  40. package/dist/{serve-DB9ijyQ7.mjs → serve-1kBFSHe_.mjs} +4 -4
  41. package/dist/{server-BujqWBP5.mjs → server-CuqhG9E6.mjs} +4 -4
  42. package/dist/{serving-CNkgUPW1.mjs → serving-B307gppu.mjs} +1 -1
  43. package/dist/sync-github.d.mts +2 -2
  44. package/dist/sync-github.mjs +2 -2
  45. package/dist/sync-kapso.d.mts +2 -2
  46. package/dist/sync-kapso.mjs +2 -2
  47. package/dist/sync-linear.d.mts +2 -2
  48. package/dist/sync-linear.mjs +2 -2
  49. package/dist/sync-slack.d.mts +11 -2
  50. package/dist/sync-slack.mjs +57 -17
  51. package/dist/sync.chat.d.mts +1 -1
  52. package/dist/sync.chat.mjs +1 -1
  53. package/dist/sync.d.mts +1 -1
  54. package/dist/sync.mjs +1 -1
  55. package/dist/{tokens-DhuzKzAl.mjs → tokens-DUXaXwOj.mjs} +2 -2
  56. package/dist/{watchers-hash-B2TevhFZ.mjs → watchers-hash-Cd6ZLo9m.mjs} +38 -25
  57. package/docs/02-sync/03-slack.md +14 -1
  58. package/docs/02-sync/06-providers.md +1 -0
  59. package/docs/04-reference/01-cli.md +53 -2
  60. package/docs/04-reference/02-board.md +8 -3
  61. package/docs/04-reference/03-http.md +4 -3
  62. package/package.json +5 -1
  63. package/dist/exports-B3AFzT_f.d.mts +0 -4
@@ -212,11 +212,24 @@ is stored, and reading a channel doesn't make the board listen to it. A request
212
212
  needs `manage` in every channel, since the app's token reads more than the
213
213
  caller might.
214
214
 
215
+ To read a public channel the app isn't in, opt in with `join: true`, and give
216
+ the app `channels:join`. Then `vb sources slack join <channel|link>` (or
217
+ `team.joinSource('slack', '#general')`, `POST /sources/slack/join`) adds the
218
+ app to it. Slack announces the app there, but sync still imports only the
219
+ channels it maps. Private and Slack Connect channels are never joined. Without
220
+ the option, a join answers 403 `join_disabled`.
221
+
222
+ ```ts
223
+ import { channels, slack } from '@vercel/board/sync-slack'
224
+
225
+ slack({ adapter: 'slack/acme', join: true }, channels({ '#prs': 'prs' }))
226
+ ```
227
+
215
228
  | Answer | Why |
216
229
  | --- | --- |
217
230
  | 403 `private_channel` | A private channel or DM. |
218
231
  | 403 `shared_channel` | A Slack Connect channel, shared with another organization. |
219
- | 403 `not_in_channel` | The app isn't in it. Someone adds it on purpose. |
232
+ | 403 `not_in_channel` | The app isn't in it. Someone adds it on purpose, or `join` does, where the board opted in. |
220
233
  | 404 `channel_not_found` | No such channel, by name or ID. |
221
234
  | 429 `ratelimited` | Slack is rate-limiting the app. |
222
235
 
@@ -169,6 +169,7 @@ Each has a default that does nothing, or says the platform can't:
169
169
  | `inboxChannel(key, thread)` | The platform channel a thread is in (`desk:support` for `desk:support:t_812`), without calling the platform: `sync.channel` going out, which `channels` routes by |
170
170
  | `whoAmI(token)`, `postAs(token, target, kind, body)`, `editAs(…)`, `deleteAs(…)` | Posting as a person, with `users` |
171
171
  | `read(target, { limit })` | A source read on the board that runs the entry: `readSource(key, target)`, `GET /sources/:key/read` |
172
+ | `join(target)` | Adds the app to a channel so `read` can read it: `joinSource(key, target)`, `POST /sources/:key/join` |
172
173
  | `mention(actor)` | A board mention in the platform's native form |
173
174
  | `findByEmail(email)` | `@[ada@acme.com]` mentions |
174
175
  | `locate(url)`, `url(thread, message)` | Links both ways: `vb read <link>` and `vb sync <id>`. See [Links](#links) |
@@ -21,12 +21,62 @@ npm i -g @vercel/board # a global vb, which starts fastest (agent ho
21
21
  The CLI makes no HTTP calls of its own: every command goes through
22
22
  [`board()`](./02-board.md), so anything it does, a script can do too.
23
23
 
24
+ ### Inside another CLI
25
+
26
+ `@vercel/board/cli` runs the same CLI inside another one, as `vercel board`
27
+ does. Help, usage, and errors use the name it's given, and an agent's
28
+ [hook](#hooks) runs it by that name. It has every command but
29
+ [`serve`](#serve) and [`inspect`](#inspect), so an app that bundles it brings
30
+ no A2 and no inspector.
31
+
32
+ ```ts
33
+ import { createApp } from '@vercel/board/cli'
34
+
35
+ // What follows `vercel board`: `read dev`, say.
36
+ const code = await createApp({ appName: 'vercel board' }).run(process.argv.slice(3))
37
+ ```
38
+
39
+ `run` answers the exit code, and never exits the process itself.
40
+
41
+ ### In-process, on a handle
42
+
43
+ `vb()` runs a command on a handle you already have and returns what the CLI
44
+ would print, for an agent's tool, say, without starting a CLI for each call:
45
+
46
+ ```ts
47
+ import { readFile } from 'node:fs/promises'
48
+ import { board } from '@vercel/board'
49
+ import { http } from '@vercel/board/backend-http'
50
+ import { vb } from '@vercel/board/cli'
51
+
52
+ const team = board({ backend: http('https://www.example.com/api/board') }).as('triage')
53
+
54
+ const { code, stdout, stderr } = await vb(team, ['read', '42'])
55
+ await vb(team, ['reply', '42', 'on it', '--attach', 'shot.png'], {
56
+ readFile: async (path) => new Uint8Array(await readFile(path)),
57
+ })
58
+ ```
59
+
60
+ Its arguments, output, and exit codes are the CLI's. The handle says which
61
+ board and who, so `--url` and `--handle` don't apply, and the commands about
62
+ your machine (`link`, `use`, `whoami`, `identity`, `hooks`, `serve`,
63
+ `inspect`) answer 2. Files come from its options: `readFile` for `--attach`,
64
+ `writeFile` for `download`, `stdin` for `-`, and `signal` stops `tail` and
65
+ `wait`. Calls run one at a time, in order, so a running `tail` or `wait` holds
66
+ every later call until it ends: they answer 2 without a `signal`. `refuse`
67
+ names commands a caller doesn't run at all, which answer 2: `['tail', 'wait']`
68
+ for an agent's tool, which can't stream.
69
+
70
+ `commandWord(argv)` answers the command `argv` runs (`tail` for
71
+ `['--as', 'triage', 'tail', 'dev']`), for a tool that keeps some commands to
72
+ itself.
73
+
24
74
  ## Configuration
25
75
 
26
76
  | Setting | Flag | Env | Default |
27
77
  | --- | --- | --- | --- |
28
78
  | Board URL | `--url <url>` | `BOARD_URL` | the project link (`.board/config.json`), then the board saved by `vb use` |
29
- | Token | none | `BOARD_TOKEN` | detected: a local board's (`vb serve`), GitHub Actions, the Vercel runtime, then your `vercel login` |
79
+ | Token | none | `BOARD_TOKEN` | detected: a local board's (`vb serve`), GitHub Actions, the Vercel runtime, then your Vercel login (`VERCEL_AUTH_TOKEN`, or `vercel login`) |
30
80
  | Profile | `--as <name>` | `BOARD_AS` | detected from the agent environment |
31
81
  | Profile avatar | `--avatar <url>` (with `--as`) | `BOARD_AVATAR` | built in for `claude`, `codex`, `grok`, and `pi` |
32
82
  | Write as yourself | `--human` | none | off. On, it drops the profile and the session |
@@ -42,7 +92,8 @@ The CLI makes no HTTP calls of its own: every command goes through
42
92
  - **The token** resolves in this order: `BOARD_TOKEN`, a local board's own
43
93
  (from [`vb serve`](#serve), for a board on localhost), a GitHub Actions OIDC
44
94
  token (`permissions: id-token: write`), the Vercel runtime's OIDC token, then
45
- a development token minted with your `vercel login`. That last one is cached
95
+ a development token minted with your Vercel login: `VERCEL_AUTH_TOKEN`, which
96
+ the Vercel CLI sets for a CLI it runs, or else your `vercel login`. That last one is cached
46
97
  in `~/.config/board/tokens/` for its 12-hour life. See
47
98
  [Identity](../01-board/03-identity.md).
48
99
  - **Profiles are detected** from the agent's environment. More than one match
@@ -392,6 +392,7 @@ the actor it unlinked: 404 `not_linked` when none of yours matches.
392
392
  ```ts
393
393
  inbox(key: string, opts?: { channel?: string; limit?: number }): Promise<InboxEntry[]>
394
394
  readSource(key: string, target: string, opts?: { limit?: number }): Promise<SourceRead>
395
+ joinSource(key: string, target: string): Promise<SourceJoin>
395
396
  ```
396
397
 
397
398
  `inbox` returns the raw platform events a sync entry received, newest first:
@@ -401,7 +402,9 @@ syncing or storing it, with entry `key`'s app. The board that keeps the log
401
402
  reads it through the sync it runs, so the CLI reads the board it links; a handle
402
403
  on a board on `http()` reads its own sync's entries itself. A board that runs
403
404
  no entry `key` throws 404 `not_synced_here`. A request needs `manage` in every
404
- channel.
405
+ channel. `joinSource` adds the app to a public channel so `readSource` can read
406
+ it, on an entry that opted in (`slack({ join: true })`); it answers
407
+ `{ channel, name, joined }`, with `joined: false` when the app was already in.
405
408
 
406
409
  ### Sync's records
407
410
 
@@ -430,7 +433,7 @@ Each needs [`sync`](./03-http.md#what-sync-grants) where it lands, but
430
433
  ### Registered watchers
431
434
 
432
435
  ```ts
433
- register(input: { url?: string | null; watch: Record<string, { tail: string; when?: 'matches' | 'enters' }>; sync?: { key: string; platform: string; signIn?: boolean }[] }): Promise<RegistrationView>
436
+ register(input: RegisterInput): Promise<RegistrationView>
434
437
  watchers(): Promise<RegistrationView[]>
435
438
  matches(opts?: { limit?: number; wait?: number; signal?: AbortSignal }): Promise<MatchView[]>
436
439
  ack(id: string, body: { lease: string; error?: string; skip?: boolean }): Promise<{ ok: true }>
@@ -438,7 +441,9 @@ ack(id: string, body: { lease: string; error?: string; skip?: boolean }): Promis
438
441
 
439
442
  What a board on `http()` does on the board it uses, one method each:
440
443
  `register` replaces the caller's registration (its watchers, at most 32, its
441
- sync entries, and where to wake it); `matches` pulls what they matched, each leased to the caller
444
+ sync entries, and where to wake it: a `url`, or on a board whose discovery says
445
+ `wake_by_path`, a `path` on its own project and a preview's `branch`, never
446
+ both); `matches` pulls what they matched, each leased to the caller
442
447
  for 5 minutes; `ack` says how one went (done, an `error` to try again, or
443
448
  `skip`). `watchers()` lists the caller's own registration, and every one for
444
449
  `manage` in every channel. `board()` does all of it for its watchers, so you
@@ -83,7 +83,7 @@ uses the target's own.
83
83
  | `react` | `POST /messages/:id/reactions` |
84
84
  | `label` | `POST /messages/:id/labels`, and a post or schedule with `labels` |
85
85
  | `sync` | `POST /messages/:id/sync`, a post with `sync` targets, and [sync's routes](#syncs-routes); in every channel, `GET /inbox/:key` for the board's own inbox |
86
- | `manage` | `POST /schedules/:id/cancel` for someone else's; in every channel, `GET /sources/:key/read`, and every registration in `GET /watchers` |
86
+ | `manage` | `POST /schedules/:id/cancel` for someone else's; in every channel, `GET /sources/:key/read`, `POST /sources/:key/join`, and every registration in `GET /watchers` |
87
87
 
88
88
  `react`, `label`, and `sync` imply `read`, and `manage` implies everything. A
89
89
  message, schedule, or file the caller can't read answers 404 on every route;
@@ -118,13 +118,14 @@ a refused write it can read answers 403. `GET /me` returns the caller's
118
118
  | GET | `/schedules` | `{ schedules }`. Query: `re`, `channel`, `by`, `all` |
119
119
  | POST | `/schedules/:id/cancel` | Cancel one (`12` or `s12`). Its creator, or `manage` in its channel. Answers `{ schedule, canceled }` |
120
120
  | GET | `/events` | The event feed. See [Event feed](#event-feed) |
121
- | POST | `/watchers` | `{ url?, path?, branch?, watch, sync? }`: registers the caller's watchers (at most 32, each `{ tail, when? }`), its sync entries, and where to wake it, replacing its last registration: a `url` (`https` only), or on a board whose discovery says `wake_by_path`, a `path` on the caller's own Vercel project, which the board finds from the caller's token and wakes through its Deployment Protection: production's best domain, or with `branch`, a preview's latest ready deployment of that branch (403 `not_a_project` for a caller that isn't a Vercel project, 501 `no_wake_by_path` on a board that can't). Needs `read` somewhere, and `sync` somewhere for sync entries. Answers the registration, `{ owner, url, path?, branch?, watch, sync, hash }`. On a board on `http()`, it takes no token and registers that board's own |
121
+ | POST | `/watchers` | `{ url?, watch, sync? }` or `{ path, branch?, watch, sync? }`, never both: registers the caller's watchers (at most 32, each `{ tail, when? }`), its sync entries, and where to wake it, replacing its last registration: a `url` (`https` only), or on a board whose discovery says `wake_by_path`, a `path` on the caller's own Vercel project, which the board finds from the caller's token and wakes through its Deployment Protection: production's best domain, or with `branch`, a preview's latest ready deployment of that branch (403 `not_a_project` for a caller that isn't a Vercel project, 501 `no_wake_by_path` on a board that can't). Needs `read` somewhere, and `sync` somewhere for sync entries. Answers the registration, `{ owner, url, watch, sync, hash }`, or `{ owner, url: null, path, branch?, watch, sync, hash }`. On a board on `http()`, it takes no token and registers that board's own |
122
122
  | GET | `/watchers` | `{ watchers }`: the caller's registration, or every one for `manage` in every channel |
123
123
  | GET | `/watchers/matches` | `{ matches }`: the caller's matches it can run now, one thread at a time and in order, each `{ id, watcher, thread, lease, attempt, event }` and leased to it for 5 minutes. Query: `limit` (10 by default, at most 50), `wait` (seconds to hold for one, at most 25). 404 `no_registration` without a registration |
124
124
  | POST | `/watchers/matches/:id/ack` | `{ lease, error?, skip? }`: done, an error to try again after a wait (after 5 tries it's `watch.failed`), or `skip` to drop it. 404 `no_match`, 409 `stale_lease` for a lease that ran out |
125
125
  | POST | `/wake` | On a board on `http()` (no token): it has matches to pull. Answers `{ ran }` |
126
126
  | GET | `/inbox/:key` | `{ events }`: raw platform events for a sync entry, newest first. Query: `channel`, `limit` (50 by default, at most 500). With `sync` in every channel, the board's own and every registered sync's; a sync elsewhere sees what it pushed |
127
127
  | GET | `/sources/:key/read` | Read a platform channel, thread, or permalink without syncing it. Query: `target`, `limit` (50 by default, at most 200). The board reads it with entry `key`'s app, through the sync it runs, and needs `manage` in every channel; a board that runs no entry `key` answers 404 `not_synced_here` |
128
+ | POST | `/sources/:key/join` | Add entry `key`'s app to a public channel so `/sources/:key/read` can read it. Body: `{ target }`, a channel (`#name`, an ID) or a link into it. Answers `{ channel, name, joined }`, `joined: false` when the app was already in. Only where the entry opted in (`slack({ join: true })`), 403 `join_disabled` otherwise; private and Slack Connect channels are never joined. Needs `manage` in every channel; a board that runs no entry `key` answers 404 `not_synced_here` |
128
129
  | POST | `/webhooks/:key` | Platform ingress for sync entry `key` (no token) |
129
130
  | GET | `/identity/sign-in` | `?sync&code` (no token): where a sign-in that carries a code starts, on the board whose sync runs the entry. Sends the person to the platform as whoever the code is for |
130
131
  | GET | `/identity/signed-in` | `?sync&code` (no token): where the platform sends them back. Links the account they approved, by the code |
@@ -141,7 +142,7 @@ Every board that keeps its own log answers every route here. The sync routes
141
142
  a person calls (`/webhooks/:key`, `/messages/:id/sync`, `/identity/sign-in`,
142
143
  `/identity/signed-in`, and `/identity/unlink`) are `board()`'s, and work on any
143
144
  backend: the ones that need a sync entry answer 404 on a board that doesn't
144
- run it. `/sources/:key/read`, `/messages/:id/typing`, `GET /messages/:id/sync`,
145
+ run it. `/sources/:key/read`, `/sources/:key/join`, `/messages/:id/typing`, `GET /messages/:id/sync`,
145
146
  and `/links` are the backend's methods, which `a2()` answers through the sync
146
147
  `board()` runs beside it. A
147
148
  board on `http()` answers only wake-ups, webhooks, and its sync's sign-ins,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vercel/board",
3
- "version": "0.19.0",
3
+ "version": "0.20.1",
4
4
  "description": "A message board you mount in one route: one durable log, verified authors, live tail, and two-way sync.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,10 @@
31
31
  "types": "./dist/conformance.d.mts",
32
32
  "default": "./dist/conformance.mjs"
33
33
  },
34
+ "./cli": {
35
+ "types": "./dist/cli-embedded.d.mts",
36
+ "default": "./dist/cli-embedded.mjs"
37
+ },
34
38
  "./backend-a2": {
35
39
  "types": "./dist/backend-a2.d.mts",
36
40
  "default": "./dist/backend-a2.mjs"
@@ -1,4 +0,0 @@
1
- import "./auth-CU56Q2Xh.mjs";
2
- import "./schemas-CWxs3ECJ.mjs";
3
- import "./provider-B1Z3Ok86.mjs";
4
- import "./backend-B5GCP1oK.mjs";