@vercel/board 0.24.0 → 0.25.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 (67) hide show
  1. package/dist/{a2-BxfKZFvJ.mjs → a2-CQ3WcqMv.mjs} +6 -6
  2. package/dist/access.mjs +3 -3
  3. package/dist/{actors-rPnEQLZu.mjs → actors-CDDhhFNY.mjs} +2 -2
  4. package/dist/{auth-5nLsx9qB.mjs → auth-BwWH3Twu.mjs} +1 -1
  5. package/dist/{backend-D0GDYiFr.d.mts → backend-DmXjVnil.d.mts} +6 -2
  6. package/dist/backend-a2.d.mts +2 -2
  7. package/dist/backend-a2.mjs +1 -1
  8. package/dist/{backend-http-rfiG-AKP.mjs → backend-http-BJ7AdK0c.mjs} +6 -6
  9. package/dist/{backend-http-Dvq2p__1.d.mts → backend-http-BsP0Xq9e.d.mts} +2 -2
  10. package/dist/{backend-http-tGUo0e8j.mjs → backend-http-Cs-UKpYp.mjs} +2 -2
  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/{backend-vercel-C3zuxmcD.mjs → backend-vercel-B0Q0K0O5.mjs} +3 -3
  16. package/dist/{backend-vercel-01QKDobk.d.mts → backend-vercel-CED673bx.d.mts} +2 -2
  17. package/dist/{backend-vercel-DM5PyMc2.mjs → backend-vercel-CG2SYa03.mjs} +3 -3
  18. package/dist/backend-vercel.d.mts +1 -1
  19. package/dist/backend-vercel.mjs +1 -1
  20. package/dist/backend-vercel.node.d.mts +1 -1
  21. package/dist/backend-vercel.node.mjs +1 -1
  22. package/dist/{chat-provider-D0k3y8zY.d.mts → chat-provider-6eYkL2ef.d.mts} +1 -1
  23. package/dist/cli-embedded.d.mts +55 -4
  24. package/dist/cli-embedded.mjs +38 -18
  25. package/dist/cli.mjs +4 -4
  26. package/dist/{client-ClznIBMg.mjs → client-BY-BCggB.mjs} +13 -7
  27. package/dist/{conformance-DQ9qVzWP.mjs → conformance-BZ53bxrG.mjs} +1 -1
  28. package/dist/conformance.d.mts +1 -1
  29. package/dist/conformance.mjs +1 -1
  30. package/dist/{engine-kHfSOXnQ.mjs → engine-SeaJ_CIY.mjs} +2 -2
  31. package/dist/{errors-CQ-fwxM3.mjs → errors-Bven0k29.mjs} +7 -3
  32. package/dist/{exports-Bw81oDDl.mjs → exports-ByvYtxFT.mjs} +6 -6
  33. package/dist/exports-CPrIkpz3.d.mts +4 -0
  34. package/dist/{index-B_hWanKF.d.mts → index-k2LfZxl-.d.mts} +2 -2
  35. package/dist/index.d.mts +3 -3
  36. package/dist/{index.default-B6oPlvEa.mjs → index.default-DmfhBZZi.mjs} +1 -1
  37. package/dist/index.mjs +6 -6
  38. package/dist/index.node.d.mts +3 -3
  39. package/dist/index.node.mjs +4 -4
  40. package/dist/inspect/app.js +1 -1
  41. package/dist/{main-CzWvRy91.mjs → main-BNkywL2P.mjs} +706 -141
  42. package/dist/{middleware-DUjyQP_a.mjs → middleware-B0J7QBq7.mjs} +1 -1
  43. package/dist/{node-CirFYqw0.mjs → node-C51_xzCb.mjs} +23 -23
  44. package/dist/{provider-D11Dfda0.d.mts → provider-C_sj4-GL.d.mts} +10 -1
  45. package/dist/{serve-DHyA-6FR.mjs → serve-DYQ64Q5R.mjs} +6 -5
  46. package/dist/{server-Cp2oIQOj.mjs → server-BgK1sgj7.mjs} +4 -4
  47. package/dist/{serving-B307gppu.mjs → serving-rXQcEgTO.mjs} +1 -1
  48. package/dist/sync-github.d.mts +2 -2
  49. package/dist/sync-kapso.d.mts +2 -2
  50. package/dist/sync-linear.d.mts +2 -2
  51. package/dist/sync-slack.d.mts +2 -2
  52. package/dist/sync.chat.d.mts +1 -1
  53. package/dist/sync.d.mts +1 -1
  54. package/dist/{tokens-DUXaXwOj.mjs → tokens-U-XRlEfq.mjs} +2 -2
  55. package/dist/{watchers-hash-C_1jGA53.mjs → watchers-hash-C4CJg7K1.mjs} +2 -2
  56. package/docs/01-board/03-identity.md +16 -7
  57. package/docs/01-board/04-messages.md +2 -2
  58. package/docs/01-board/05-reading.md +16 -8
  59. package/docs/01-board/07-schedules.md +2 -2
  60. package/docs/01-board/09-access.md +5 -4
  61. package/docs/03-guides/01-coding-agents.md +4 -2
  62. package/docs/03-guides/03-ci.md +2 -2
  63. package/docs/04-reference/01-cli.md +78 -19
  64. package/docs/04-reference/02-board.md +2 -1
  65. package/docs/04-reference/03-http.md +4 -0
  66. package/package.json +1 -1
  67. package/dist/exports-87_yESpp.d.mts +0 -4
@@ -1,5 +1,5 @@
1
1
  import { m as MESSAGE_TEXT_MAX, p as Labels } from "./schemas-Be08nyfW.mjs";
2
- import { t as HttpError } from "./errors-CQ-fwxM3.mjs";
2
+ import { t as HttpError } from "./errors-Bven0k29.mjs";
3
3
  import { z } from "zod";
4
4
  //#region src/server/middleware.ts
5
5
  /** A write the middleware didn't let through: the API answers 422 with what it responded. */
@@ -1,4 +1,4 @@
1
- import { t as BoardError } from "./client-ClznIBMg.mjs";
1
+ import { t as BoardError } from "./client-BY-BCggB.mjs";
2
2
  import { execFile } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
4
  import { mkdir, readFile, writeFile } from "node:fs/promises";
@@ -99,7 +99,7 @@ function detectAgent(env = process.env) {
99
99
  profile: named,
100
100
  session: null
101
101
  });
102
- if (found.length > 1) throw new BoardError("ambiguous_agent", `several agent environments detected (${found.map((a) => a.profile).join(", ")}): choose one with a profile, or none`, 0);
102
+ if (found.length > 1) throw new BoardError("ambiguous_agent", `Several coding agents' environments are set (${found.map((a) => a.profile).join(", ")}), so it can't tell which one is writing.`, 0, { hint: "Pick one with a profile, or none to write as yourself." });
103
103
  return found[0] ?? null;
104
104
  }
105
105
  const expiry = (token) => {
@@ -157,7 +157,7 @@ async function personToken(audience, discovery) {
157
157
  return filled;
158
158
  }
159
159
  if (cached) return cached;
160
- if (!discovery.project_id) throw new BoardError("no_project", `the board at ${discovery.url} doesn't know its Vercel project`, 0);
160
+ if (!discovery.project_id) throw new BoardError("no_project", `The board at ${discovery.url} doesn't know its Vercel project, so there's no Vercel login token to mint for it.`, 0, { hint: "Pass a token it accepts in BOARD_TOKEN." });
161
161
  const authToken = await vercelLogin().catch(() => null);
162
162
  const project = await boardProject(audience, discovery, authToken);
163
163
  let development;
@@ -167,7 +167,7 @@ async function personToken(audience, discovery) {
167
167
  method: "POST",
168
168
  headers: { authorization: `Bearer ${authToken}` }
169
169
  });
170
- if (response.status === 403 || response.status === 404) throw new BoardError("no_access", "your Vercel account can't access this board's project: ask for access to it", response.status);
170
+ if (response.status === 403 || response.status === 404) throw new BoardError("no_access", "Your Vercel account can't access this board's project.", response.status, { hint: "Ask someone on the project's team for access to it." });
171
171
  if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
172
172
  development = (await response.json()).token;
173
173
  } catch (error) {
@@ -210,8 +210,8 @@ async function boardProject(url, discovery, authToken) {
210
210
  });
211
211
  return response.ok ? await response.json() : null;
212
212
  } : vercelCliApi, host);
213
- if (!found) throw new BoardError("no_access", `your Vercel login can't see the project ${host} belongs to: run \`vercel login\` or ask for access to it`, 0);
214
- if (found.projectId !== discovery.project_id) throw new BoardError("wrong_project", `the board at ${url} says its project is ${discovery.project_id}, but it's a deployment of ${found.projectId}, so vb won't sign in to it`, 0);
213
+ if (!found) throw new BoardError("no_access", `Your Vercel login can't see the project ${host} belongs to.`, 0, { hint: "Log in as someone on the project's team (vercel login), or ask for access to it." });
214
+ if (found.projectId !== discovery.project_id) throw new BoardError("wrong_project", `The board at ${url} says its project is ${discovery.project_id}, but it's a deployment of ${found.projectId}, so no token is minted for it.`, 0);
215
215
  return {
216
216
  id: found.projectId,
217
217
  team: found.ownerId.startsWith("team_") ? found.ownerId : null
@@ -230,7 +230,7 @@ async function tokenFromVercelCli(project) {
230
230
  const { stdout } = await promisify(execFile)("vercel", args, { timeout: 3e4 });
231
231
  return JSON.parse(stdout).token;
232
232
  } catch {
233
- throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
233
+ throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
234
234
  }
235
235
  }
236
236
  /** Your Vercel username, display name, and avatar (Vercel serves avatars publicly). */
@@ -252,13 +252,13 @@ async function githubActionsToken(audience) {
252
252
  const url = new URL(process.env.ACTIONS_ID_TOKEN_REQUEST_URL);
253
253
  url.searchParams.set("audience", audience);
254
254
  const response = await fetch(url, { headers: { authorization: `bearer ${process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN}` } });
255
- if (!response.ok) throw new BoardError("no_token", `GitHub Actions refused an OIDC token (${response.status}): does the job have \`permissions: id-token: write\`?`, 0);
255
+ if (!response.ok) throw new BoardError("no_token", `GitHub Actions refused an OIDC token (${response.status}).`, 0, { hint: "Give the job `permissions: id-token: write`." });
256
256
  return (await response.json()).value;
257
257
  }
258
258
  /** The board the environment points at: `BOARD_URL`. */
259
259
  function boardUrl() {
260
260
  const url = process.env.BOARD_URL?.replace(/\/+$/, "");
261
- if (!url) throw new BoardError("no_board", "no board URL: set BOARD_URL", 0);
261
+ if (!url) throw new BoardError("no_board", "There's no board URL: BOARD_URL isn't set.", 0, { hint: "Set BOARD_URL, or pass the URL to http()." });
262
262
  return url;
263
263
  }
264
264
  /**
@@ -272,9 +272,9 @@ function connect(url, options = {}) {
272
272
  const doFetch = guarded(url, options.fetch ?? ((input, init) => globalThis.fetch(input, init)), options.token);
273
273
  let discovery;
274
274
  const discover = () => discovery ??= doFetch(url).then(async (response) => {
275
- if (!response.ok) throw new BoardError("no_board", `no board at ${url} (${response.status})`, response.status);
275
+ if (!response.ok) throw new BoardError("no_board", `There's no board at ${url}: it answered ${response.status}.`, response.status);
276
276
  const found = await response.json();
277
- if (found.audience !== url) throw new BoardError("wrong_audience", `the board at ${url} says its URL is ${found.audience}: use that URL`, 0);
277
+ if (found.audience !== url) throw new BoardError("wrong_audience", `The board at ${url} says its URL is ${found.audience}.`, 0, { hint: `Use ${found.audience}.` });
278
278
  return found;
279
279
  });
280
280
  let cached = null;
@@ -388,7 +388,7 @@ async function hostedToken(url) {
388
388
  try {
389
389
  return await getVercelOidcToken({ audience: url });
390
390
  } catch (cause) {
391
- throw Object.assign(new BoardError("no_token", `cannot obtain Vercel OIDC credentials for ${url}: enable OIDC on the calling project or supply token / BOARD_TOKEN; configure Deployment Protection Trusted Sources for its project and environment`, 0), { cause });
391
+ throw Object.assign(new BoardError("no_token", `This deployment can't get a Vercel OIDC token for ${url}.`, 0, { hint: "Enable OIDC on its project, or pass a token (BOARD_TOKEN), and add its project to the board project's Deployment Protection Trusted Sources." }), { cause });
392
392
  }
393
393
  }
394
394
  /** A host this deployment answers on, as Vercel's system variables name them (bare hosts). */
@@ -414,7 +414,7 @@ function guarded(url, doFetch, suppliedToken) {
414
414
  return await through(doFetch, input, init, fromEnv);
415
415
  } catch (error) {
416
416
  if (!(error instanceof Wall)) throw error;
417
- if (process.env.VERCEL !== "1") throw new BoardError("protected", `${url} is behind Vercel Deployment Protection and rejected VERCEL_AUTOMATION_BYPASS_SECRET; check it against ${WHERE}`, error.status);
417
+ if (process.env.VERCEL !== "1") throw new BoardError("protected", `${url} is behind Vercel Deployment Protection, and it rejected VERCEL_AUTOMATION_BYPASS_SECRET.`, error.status, { hint: `Check VERCEL_AUTOMATION_BYPASS_SECRET against ${WHERE}.` });
418
418
  envRejected = true;
419
419
  }
420
420
  if (process.env.VERCEL === "1") {
@@ -423,7 +423,7 @@ function guarded(url, doFetch, suppliedToken) {
423
423
  headers.set("x-vercel-trusted-oidc-idp-token", token);
424
424
  }
425
425
  return through(doFetch, input, init, null).catch((error) => {
426
- throw error instanceof Wall ? new BoardError("protected", `${url} rejected Vercel OIDC access: configure Deployment Protection Trusted Sources for the calling project and environment`, error.status) : error;
426
+ throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection, and it rejected this deployment's Vercel OIDC token.`, error.status, { hint: "Add this project and environment to the board project's Deployment Protection Trusted Sources." }) : error;
427
427
  });
428
428
  }
429
429
  const used = await (secret ??= readBypass(host));
@@ -445,7 +445,7 @@ function guarded(url, doFetch, suppliedToken) {
445
445
  return through(doFetch, input, init, found);
446
446
  });
447
447
  } catch (error) {
448
- throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection and its project's automation bypass didn't get through; set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE})`, error.status) : error;
448
+ throw error instanceof Wall ? new BoardError("protected", `${url} is behind Vercel Deployment Protection, and its project's automation bypass didn't get through.`, error.status, { hint: `Set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE}).` }) : error;
449
449
  }
450
450
  };
451
451
  }
@@ -475,7 +475,7 @@ async function through(doFetch, input, init, bypass) {
475
475
  const next = new URL(location, target);
476
476
  if (next.pathname.startsWith("/sso-api")) throw new Wall(response.status);
477
477
  if (method === "GET" || method === "HEAD") {
478
- if (hops >= 20) throw new BoardError("too_many_redirects", `too many redirects from ${target}`, response.status);
478
+ if (hops >= 20) throw new BoardError("too_many_redirects", `${target} redirected more than 20 times.`, response.status);
479
479
  if (next.origin !== new URL(target).origin) {
480
480
  headers = new Headers(headers);
481
481
  headers.delete("authorization");
@@ -517,7 +517,7 @@ const automationBypass = (bypasses) => Object.entries(bypasses ?? {}).find(([, v
517
517
  * `protectionBypass` has it. When there's none, it's created.
518
518
  */
519
519
  async function projectBypass(url, host, doFetch) {
520
- const blocked = (why) => new BoardError("protected", `${url} is behind Vercel Deployment Protection, and your Vercel login ${why}, or set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE})`, 0);
520
+ const blocked = (why, fix) => new BoardError("protected", `${url} is behind Vercel Deployment Protection, and your Vercel login ${why}.`, 0, { hint: `${fix}, or set VERCEL_AUTOMATION_BYPASS_SECRET (${WHERE}).` });
521
521
  let api;
522
522
  try {
523
523
  const token = await vercelLogin();
@@ -536,19 +536,19 @@ async function projectBypass(url, host, doFetch) {
536
536
  api = vercelCliApi;
537
537
  }
538
538
  const project = await findProject(api, host);
539
- if (!project) throw blocked("can't see its project: run `vercel login` or ask for access");
539
+ if (!project) throw blocked("can't see its project", "Log in as someone on its project's team (vercel login), ask for access");
540
540
  const scope = project.ownerId.startsWith("team_") ? `?teamId=${project.ownerId}` : "";
541
541
  const details = await api("GET", `/v9/projects/${project.projectId}${scope}`);
542
- if (!details) throw blocked("can't read its project's settings: ask for access");
542
+ if (!details) throw blocked("can't read its project's settings", "Ask for access to its project");
543
543
  const existing = automationBypass(details.protectionBypass);
544
544
  if (existing) return {
545
545
  secret: existing,
546
546
  created: false
547
547
  };
548
- if (!details.ssoProtection && !details.passwordProtection && !details.passport) throw new BoardError("no_board", `no board at ${url}: it answered with a web page, and its Vercel project isn't protected`, 0);
548
+ if (!details.ssoProtection && !details.passwordProtection && !details.passport) throw new BoardError("no_board", `There's no board at ${url}: it answered with a web page, and its Vercel project isn't protected.`, 0);
549
549
  const created = await api("PATCH", `/v1/projects/${project.projectId}/protection-bypass${scope}`);
550
550
  const secret = automationBypass(created?.protectionBypass);
551
- if (!secret) throw blocked("can't read or create its project's automation bypass: ask a project admin");
551
+ if (!secret) throw blocked("can't read or create its project's automation bypass", "Ask a project admin to create one");
552
552
  process.stderr.write(`created a Protection Bypass for Automation on the Vercel project ${details?.name ?? project.projectId}\n`);
553
553
  return {
554
554
  secret,
@@ -579,7 +579,7 @@ async function findProject(api, host) {
579
579
  let vercelCli;
580
580
  const vercelCliApi = async (method, path) => {
581
581
  vercelCli ??= promisify(execFile)("vercel", ["whoami", "--non-interactive"], { timeout: 1e4 }).then(() => true, () => false);
582
- if (!await vercelCli) throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
582
+ if (!await vercelCli) throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
583
583
  const args = [
584
584
  "api",
585
585
  path,
@@ -595,7 +595,7 @@ const vercelCliApi = async (method, path) => {
595
595
  run.child.stdin?.end(method === "PATCH" ? "{}" : "");
596
596
  return JSON.parse((await run).stdout);
597
597
  } catch (error) {
598
- if (error.code === "ENOENT") throw new BoardError("not_logged_in", "not signed in to Vercel: run `vercel login`", 0);
598
+ if (error.code === "ENOENT") throw new BoardError("not_logged_in", "You're not logged in to Vercel.", 0, { hint: "Run vercel login." });
599
599
  return null;
600
600
  }
601
601
  };
@@ -919,10 +919,12 @@ type SourceJoin = {
919
919
  name: string | null;
920
920
  joined: boolean;
921
921
  };
922
+ /** A failure: `code` to branch on, `message` to read, and `hint`, when the board knows one, for what fixes it. */
922
923
  type ApiError = {
923
924
  error: {
924
925
  code: string;
925
926
  message: string;
927
+ hint?: string;
926
928
  };
927
929
  };
928
930
  /** A raw platform event, as `GET /inbox/:key` returns it. */
@@ -1696,10 +1698,17 @@ declare const routes: {
1696
1698
  type RouteName = keyof typeof routes;
1697
1699
  //#endregion
1698
1700
  //#region src/client/client.d.ts
1701
+ /**
1702
+ * What went wrong: `message` says what and why, `code` is what to branch on, and `hint`, when the
1703
+ * handle knows one, says what fixes it (`Set VERCEL_AUTOMATION_BYPASS_SECRET…`).
1704
+ */
1699
1705
  declare class BoardError extends Error {
1700
1706
  readonly code: string;
1701
1707
  readonly status: number;
1702
- constructor(code: string, message: string, status: number);
1708
+ readonly hint: string | undefined;
1709
+ constructor(code: string, message: string, status: number, options?: {
1710
+ hint?: string;
1711
+ });
1703
1712
  }
1704
1713
  type Resolvable<T> = T | (() => T | Promise<T>);
1705
1714
  /** A profile name, `{ name, avatar }`, or null for no profile. */
@@ -1,7 +1,8 @@
1
- import { i as localTokenFile } from "./node-CirFYqw0.mjs";
2
- import { t as board } from "./server-Cp2oIQOj.mjs";
3
- import { t as oidcTokens } from "./tokens-DUXaXwOj.mjs";
4
- import { n as fsBlob, t as a2 } from "./a2-BxfKZFvJ.mjs";
1
+ import { t as BoardError } from "./client-BY-BCggB.mjs";
2
+ import { i as localTokenFile } from "./node-C51_xzCb.mjs";
3
+ import { t as board } from "./server-BgK1sgj7.mjs";
4
+ import { t as oidcTokens } from "./tokens-U-XRlEfq.mjs";
5
+ import { n as fsBlob, t as a2 } from "./a2-CQ3WcqMv.mjs";
5
6
  import { t as localScheduler } from "./local-DhkrAqPP.mjs";
6
7
  import { mkdir, rm, writeFile } from "node:fs/promises";
7
8
  import { userInfo } from "node:os";
@@ -53,7 +54,7 @@ async function serveLocal(options) {
53
54
  port = await listen(server, options.port ?? 4100);
54
55
  } catch (error) {
55
56
  await store.close();
56
- if (error.code === "EADDRINUSE") throw new Error(`port ${options.port ?? 4100} is taken: is a board already running? Pick another with --port`);
57
+ if (error.code === "EADDRINUSE") throw new BoardError("port_taken", `Port ${options.port ?? 4100} is taken. Is a board already running there?`, 0);
57
58
  throw error;
58
59
  }
59
60
  const url = `http://127.0.0.1:${port}/api/board`;
@@ -1,5 +1,5 @@
1
- import { t as createBoard } from "./exports-Bw81oDDl.mjs";
2
- import { t as http } from "./backend-http-tGUo0e8j.mjs";
1
+ import { t as createBoard } from "./exports-ByvYtxFT.mjs";
2
+ import { t as http } from "./backend-http-Cs-UKpYp.mjs";
3
3
  //#region src/server/index.ts
4
4
  /**
5
5
  * The handle on a board. With a `backend`, the board lives there: `a2()` keeps it in this
@@ -9,8 +9,8 @@ import { t as http } from "./backend-http-tGUo0e8j.mjs";
9
9
  function board(options = {}) {
10
10
  return createBoard(options, {
11
11
  http: () => http(),
12
- sync: () => import("./engine-kHfSOXnQ.mjs"),
13
- serving: () => import("./serving-B307gppu.mjs")
12
+ sync: () => import("./engine-SeaJ_CIY.mjs"),
13
+ serving: () => import("./serving-rXQcEgTO.mjs")
14
14
  });
15
15
  }
16
16
  //#endregion
@@ -1,2 +1,2 @@
1
- import { i as ownTeamId, n as discoveryIdentity, r as ownProjectId, t as oidcTokens } from "./tokens-DUXaXwOj.mjs";
1
+ import { i as ownTeamId, n as discoveryIdentity, r as ownProjectId, t as oidcTokens } from "./tokens-U-XRlEfq.mjs";
2
2
  export { discoveryIdentity, oidcTokens, ownProjectId, ownTeamId };
@@ -1,5 +1,5 @@
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-D11Dfda0.mjs";
2
- import { n as ChatSyncProvider } from "./chat-provider-D0k3y8zY.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-C_sj4-GL.mjs";
2
+ import { n as ChatSyncProvider } from "./chat-provider-6eYkL2ef.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/github.d.ts
@@ -1,5 +1,5 @@
1
- import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-D11Dfda0.mjs";
2
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
1
+ import { S as SyncUnit, f as SyncContext, y as SyncFn } from "./provider-C_sj4-GL.mjs";
2
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/kapso.d.ts
@@ -1,5 +1,5 @@
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-D11Dfda0.mjs";
2
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
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-C_sj4-GL.mjs";
2
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
3
3
  import { z } from "zod";
4
4
  import { Adapter } from "chat";
5
5
  //#region src/sync/linear.d.ts
@@ -1,6 +1,6 @@
1
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-D11Dfda0.mjs";
3
- import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-D0k3y8zY.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-C_sj4-GL.mjs";
3
+ import { n as ChatSyncProvider, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
4
4
  import { z } from "zod";
5
5
  import { Adapter } from "chat";
6
6
  //#region src/sync/slack.d.ts
@@ -1,2 +1,2 @@
1
- import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-D0k3y8zY.mjs";
1
+ import { i as externalAuthor, n as ChatSyncProvider, r as ChatTransport, t as ChatInput } from "./chat-provider-6eYkL2ef.mjs";
2
2
  export { ChatInput, ChatSyncProvider, ChatTransport, externalAuthor };
package/dist/sync.d.mts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { d as ExternalFile, l as ExternalAuthor, u as ExternalEvent } from "./folds-CRYY14kc.mjs";
2
- import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-D11Dfda0.mjs";
2
+ import { A as SyncOverrides, C as channels, D as EventBase, E as render, M as SyncRoot, N as envelope, O as ProviderEvent, P as eventSchema, S as SyncUnit, T as mirror, _ as withUserTokens, a as ProviderOptions, b as SyncIn, c as ReceivedEvent, d as ResolvedTarget, f as SyncContext, g as connectUserTokens, h as UserTokens, i as ProviderFile, j as SyncResult, k as RootOverride, l as RenderInput, m as UnreadableError, n as FileDelivery, o as ProviderPost, p as SyncProvider, r as ProviderDelivery, s as Receive, u as Renderer, v as Sync, w as into, x as SyncOut, y as SyncFn } from "./provider-C_sj4-GL.mjs";
3
3
  export { EventBase, type ExternalAuthor, ExternalEvent, type ExternalFile, type FileDelivery, type ProviderDelivery, type ProviderEvent, type ProviderFile, type ProviderOptions, type ProviderPost, type Receive, ReceivedEvent, type RenderInput, type Renderer, ResolvedTarget, type RootOverride, type Sync, type SyncContext, type SyncFn, type SyncIn, type SyncOut, SyncOverrides, SyncProvider, type SyncResult, type SyncRoot, type SyncUnit, UnreadableError, type UserTokens, channels, connectUserTokens, envelope, eventSchema, into, mirror, render, withUserTokens };
@@ -1,5 +1,5 @@
1
- import { l as unauthorized } from "./errors-CQ-fwxM3.mjs";
2
- import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-5nLsx9qB.mjs";
1
+ import { l as unauthorized } from "./errors-Bven0k29.mjs";
2
+ import { f as markVerified, m as trustedIssuers, n as TOKENS, r as VERCEL_ISSUER, u as issuerName } from "./auth-BwWH3Twu.mjs";
3
3
  import { createRemoteJWKSet, decodeJwt, jwtVerify } from "jose";
4
4
  //#region src/server/identity.ts
5
5
  let fromDisk;
@@ -1,7 +1,7 @@
1
1
  import { n as DIRECT_UPLOAD_BYTES_MAX, t as ATTACHMENT_BYTES_MAX } from "./files-SniSmeEr.mjs";
2
2
  import { S as storable, a as AttachmentId, c as ChannelPath, d as LabelKey, g as MessageText, l as Emoji, m as MESSAGE_TEXT_MAX, n as ActorId, p as Labels, x as platformAvatar } from "./schemas-Be08nyfW.mjs";
3
3
  import { a as Link, i as ExternalTarget, n as ExternalEvent, o as Origin, t as ExternalAuthor } from "./external-BV_cUncs.mjs";
4
- import { r as badRequest, t as HttpError } from "./errors-CQ-fwxM3.mjs";
4
+ import { r as badRequest, t as HttpError } from "./errors-Bven0k29.mjs";
5
5
  import { o as options } from "./commands-BtL4jFq8.mjs";
6
6
  import { z } from "zod";
7
7
  //#region src/api/routes.ts
@@ -662,7 +662,7 @@ const routes = {
662
662
  const SYNC = "@sync";
663
663
  //#endregion
664
664
  //#region src/version.ts
665
- const VERSION = "0.24.0";
665
+ const VERSION = "0.25.0";
666
666
  //#endregion
667
667
  //#region src/server/routing.ts
668
668
  /** board()'s own routes: the files it serves, and sync's, which run where board() does. */
@@ -67,18 +67,26 @@ source vercel login
67
67
  token expires in 11h 42m
68
68
  ```
69
69
 
70
- When it can't, it says what fixes it:
70
+ When it can't, it says why, then what fixes it:
71
71
 
72
72
  ```text
73
- error: not signed in to Vercel: run `vercel login`
74
- error: your Vercel login can't see the project www.example.com belongs to: run `vercel login` or ask for access to it
75
- error: your Vercel account can't access this board's project: ask for access to it
73
+ $ vb whoami
74
+ error: You're not logged in to Vercel.
75
+
76
+ Log in:
77
+ vercel login
78
+ ```
79
+
80
+ ```text
81
+ error: Your Vercel login can't see the project www.example.com belongs to.
82
+
83
+ Log in as someone on the project's team (vercel login), or ask for access to it.
76
84
  ```
77
85
 
78
86
  When the board's host is another project's deployment, it gets no token:
79
87
 
80
88
  ```text
81
- error: the board at https://www.example.com/api/board says its project is prj_abc123, but it's a deployment of prj_def456, so vb won't sign in to it
89
+ error: The board at https://www.example.com/api/board says its project is prj_abc123, but it's a deployment of prj_def456, so no token is minted for it.
82
90
  ```
83
91
 
84
92
  ## How the board verifies
@@ -248,10 +256,11 @@ vb reply 812 "@ada can you look? cc @julian/claude"
248
256
 
249
257
  ```text
250
258
  $ vb post errors-www "@ada the rollback is live"
251
- error: ambiguous mention @ada: use @[slack:user:U0456] (ada) or @[vercel:user:u_9f] (ada)
259
+ error: Can't post to errors-www.
260
+ ambiguous mention @ada: use @[slack:user:U0456] (ada) or @[vercel:user:u_9f] (ada)
252
261
  ```
253
262
 
254
- An `@[actor-id]` the board doesn't know fails with `error: unknown actor`. For
263
+ An `@[actor-id]` the board doesn't know fails too, the board saying `unknown actor`. For
255
264
  a caller that can't read every channel, an ambiguous mention lists no
256
265
  candidates: `use @[actor-id]`. A caller that can't read the channel it writes
257
266
  in gets no warning for a handle no one has, since it would say which handles
@@ -142,8 +142,8 @@ vb download nXHY2NjUHuzE5F1QpXyBdQ # saves after.png here; a path, or
142
142
  board's copy. See [Sync](../02-sync/01-overview.md).
143
143
  - **They need a blob store.** A board on `a2()` without one keeps no files:
144
144
  an upload, a post or schedule that carries files, and a download answer 501
145
- `no_files`, and `vb` prints
146
- ``error: this board keeps no files: pass `blob` to `a2()` ``. Sync brings a
145
+ `no_files`, and `vb` says it can't, with the board's words under it:
146
+ ``this board keeps no files: pass `blob` to `a2()` ``. Sync brings a
147
147
  platform's files over as links to where they live instead. See
148
148
  [Mounting](./01-mounting.md#without-a-blob-store).
149
149
 
@@ -28,12 +28,19 @@ errors-www · 2 threads · cursor 815
28
28
  pending [scheduled messages](./07-schedules.md).
29
29
  - **`vb read`** with no channel reads across every channel, and each summary
30
30
  names its channel.
31
- - **`--json`** prints one `{ message, replies, last }` per line (JSONL): the
32
- root, its reply count, and its latest reply or `null`. `vb read <id> --json`
33
- prints one message per line.
31
+ - **`--json`** prints `{ threads, cursor }`, each thread `{ message, replies,
32
+ last }`: the root, its reply count, and its latest reply or `null`.
34
33
 
35
- A channel that doesn't exist is an error (`no channel errors-wwww`), so a typo
36
- doesn't read as an empty channel.
34
+ A channel that doesn't exist is an error, so a typo doesn't read as an empty
35
+ channel:
36
+
37
+ ```text
38
+ $ vb read errors-wwww
39
+ error: Couldn't find channel errors-wwww. Did you mean errors-www?
40
+
41
+ See its channels:
42
+ vb channels
43
+ ```
37
44
 
38
45
  ## One thread
39
46
 
@@ -49,7 +56,7 @@ $ vb read 812
49
56
  ```
50
57
 
51
58
  `vb read <id>` prints every message in the thread, oldest first, and `--json`
52
- prints one message per line. Filters narrow it to the root and the replies that
59
+ prints `{ message, replies, cursor, schedules }`. Filters narrow it to the root and the replies that
53
60
  match: `vb read 812 --by ada --label status=investigating`. Each reaction
54
61
  names its first three people and counts the rest (`👀 ada, claude, codex +9`);
55
62
  `--json` has them all.
@@ -125,8 +132,9 @@ vb wait --re 812 --timeout 30m
125
132
  ```
126
133
 
127
134
  `vb wait [channel] [filters]` blocks until the next matching event, prints it,
128
- and exits 0. If nothing matches in time it prints `timed out` on stderr and
129
- exits 124, the GNU `timeout` convention. The timeout defaults to `30m` and takes
135
+ and exits 0. If nothing matches in time it says so on stderr
136
+ (`error: Nothing matched within 30m.`) and exits 124, the GNU `timeout`
137
+ convention. The timeout defaults to `30m` and takes
130
138
  `30s`, `10m`, `1h`, or `1d`.
131
139
 
132
140
  Like `tail`, it starts at the head. Pass `--after` to start from a known
@@ -105,8 +105,8 @@ await team.cancelSchedule(reminder.id) // { schedule, canceled }
105
105
 
106
106
  - **It runs on the scheduler.** A board on `a2()` without one refuses:
107
107
  `POST /schedules`, `schedule()`, and `vb schedule` answer 501
108
- `no_scheduler`, and `vb` prints
109
- ``error: this board has no scheduler: pass `scheduler` to `a2()` to send later``.
108
+ `no_scheduler`, and `vb` says it can't, with the board's words under it:
109
+ ``this board has no scheduler: pass `scheduler` to `a2()` to send later``.
110
110
  See [Without a scheduler](./01-mounting.md#without-a-scheduler). In
111
111
  production, pass `vercelQueues()` or `qstash()` and mount
112
112
  [its route](./01-mounting.md#the-scheduler-route). On a dev server, in
@@ -25,14 +25,15 @@ The board's environment is the one Vercel runs it in (`VERCEL_TARGET_ENV`,
25
25
  which names custom environments, or `VERCEL_ENV`). A production board trusts
26
26
  its production and refuses its previews, which run any branch's code. A
27
27
  preview board trusts its own previews, so a preview can call itself. Outside
28
- Vercel, only people get in without a rule.
28
+ Vercel, only people get in without a rule. A refused token gets a 403 that
29
+ says what would let it in:
29
30
 
30
31
  ```text
31
- error: this token can't use this board: it runs in production, so its preview deployments need vercelProject({ project: 'self', environments: ['preview'], can })
32
+ this token can't use this board: it runs in production, so its preview deployments need vercelProject({ project: 'self', environments: ['preview'], can })
32
33
  ```
33
34
 
34
35
  A token no rule matches is refused the same way, with the rule that would let
35
- it in: `error: this token can't use this board: add githubActions({ repository:
36
+ it in: `this token can't use this board: add githubActions({ repository:
36
37
  'acme/www', can }) to access`.
37
38
 
38
39
  ## Configure it
@@ -408,7 +409,7 @@ Reads show only what the caller can read, everywhere:
408
409
  - **What you can't read answers 404,** as if it didn't exist. On `a2()`,
409
410
  message IDs are sequential, so a 403 would confirm a message is there.
410
411
  - **A refused write you can read answers 403** and names what's missing:
411
- `error: this token can't label in dev`.
412
+ `this token can't label in dev`.
412
413
  - **A channel the request names is yours to know,** so a post to a channel you
413
414
  can't post in is 403, read or not.
414
415
  - **Every check is against the channel the board looked up.** A reply's
@@ -21,7 +21,7 @@ which team.
21
21
  - **Credentials stay with you.** You sign in with your own `vercel login`,
22
22
  and tokens are cached per user. Access to the board is access to its Vercel
23
23
  project: an account without it gets
24
- `error: your Vercel account can't access this board's project: ask for access to it`.
24
+ `error: Your Vercel account can't access this board's project.`
25
25
  - **The URL picks a board, not a channel.** Agents choose channels per task.
26
26
 
27
27
  Every command finds the board in this order, and the first match wins:
@@ -61,7 +61,9 @@ saying so), keep it per host in `~/.config/board/bypass/`, and send it as
61
61
  sent instead. When neither gets through, the CLI says why:
62
62
 
63
63
  ```text
64
- error: https://team.example/api/board is behind Vercel Deployment Protection, and your Vercel login can't see its project: run `vercel login` or ask for access, or set VERCEL_AUTOMATION_BYPASS_SECRET (Project → Settings → Deployment Protection → Protection Bypass for Automation)
64
+ error: https://team.example/api/board is behind Vercel Deployment Protection, and your Vercel login can't see its project.
65
+
66
+ Log in as someone on its project's team (vercel login), ask for access, or set VERCEL_AUTOMATION_BYPASS_SECRET (Project → Settings → Deployment Protection → Protection Bypass for Automation).
65
67
  ```
66
68
 
67
69
  ## Install the skill
@@ -72,8 +72,8 @@ export const GET = team.fetch, POST = team.fetch
72
72
  `githubActions()` matches the token's `repository`, plus `workflow`, `event`
73
73
  (`push`, `pull_request`, or a list), and `ref` when you give them. The
74
74
  workflow posts as `github:repo:<repository_id>`, with the handle
75
- `acme/www`. A workflow that matches no rule gets
76
- `error: this token can't use this board: add githubActions({ repository: 'acme/www', can }) to access`.
75
+ `acme/www`. A workflow that matches no rule gets a 403:
76
+ `this token can't use this board: add githubActions({ repository: 'acme/www', can }) to access`.
77
77
 
78
78
  `can` says what it may do where. This one posts and labels in `errors-www`, for
79
79
  the step above, and reads and posts in `deploys`, which the wait below needs.