@cavuno/board 1.2.0 → 1.3.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.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: cavuno-board-client
3
+ description: Create and configure the @cavuno/board client — baseUrl and the pk_ board identifier, global headers, request/response hooks, per-call FetchOptions caching passthrough, the client.fetch escape hatch, and the rule that keeps one shared instance safe under SSR.
4
+ ---
5
+
6
+ # The Board API client
7
+
8
+ `createBoardClient` returns a typed client whose namespaces (`jobs`, `companies`, `blog`, `auth`, `me`, …) all route through one request pipeline: board-identifier base path, default headers, bearer token from storage, and your hooks.
9
+
10
+ ## When to use
11
+
12
+ - Creating the client instance your whole app shares.
13
+ - Adding global headers, logging, request/response hooks, or framework caching.
14
+ - Calling an endpoint the SDK doesn't expose yet (`client.fetch`).
15
+
16
+ ## When not to use
17
+
18
+ - Per-surface usage (listing jobs, auth) — see the surface skills; they assume the client exists.
19
+
20
+ ## Create the client
21
+
22
+ ```ts
23
+ import { createBoardClient } from '@cavuno/board';
24
+
25
+ const board = createBoardClient({
26
+ baseUrl: 'https://api.cavuno.com',
27
+ board: 'pk_a8f3...', // pk_ key (preferred) | boards_ id | slug
28
+ });
29
+ ```
30
+
31
+ Use the `pk_…` publishable key for `board`, not the slug: the slug is operator-mutable and renames break deployed frontends. The `pk_…` key is immutable and client-safe.
32
+
33
+ ## Configuration
34
+
35
+ ```ts no-check
36
+ const board = createBoardClient({
37
+ baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
38
+ board: process.env.PUBLIC_CAVUNO_BOARD!,
39
+ globalHeaders: { 'Accept-Language': 'en' },
40
+ onRequest: async (req) => req, // mutate/replace the request (locale, URL rewrite)
41
+ onResponse: async (res, req) => {}, // side effects only (logging, analytics)
42
+ logger: console,
43
+ auth: { storage: 'memory' }, // see cavuno-board-auth
44
+ });
45
+ ```
46
+
47
+ The `onRequest`/`onResponse` hooks are first-class — do not monkey-patch the client. `onResponse` receives a **clone** of the response, so reading its body never disturbs the SDK's own parsing.
48
+
49
+ ## Per-call FetchOptions ride straight to fetch
50
+
51
+ Every method takes a trailing `options?` of type `FetchOptions` (`Omit<RequestInit,'body'>` plus `query`). Anything besides `body`/`query` passes through to `fetch` untouched, so framework caching works with zero SDK knowledge:
52
+
53
+ ```ts snippet
54
+ // Next.js ISR
55
+ await board.jobs.list({ limit: 20 }, { next: { revalidate: 60, tags: ['jobs'] } });
56
+ // Cloudflare Workers
57
+ await board.jobs.list({ limit: 20 }, { cf: { cacheTtl: 60 } } as never);
58
+ // Standard fetch + abort
59
+ const ac = new AbortController();
60
+ await board.jobs.list({ limit: 20 }, { cache: 'force-cache', signal: ac.signal });
61
+ ```
62
+
63
+ ## One shared instance is SSR-safe — if state stays per-call
64
+
65
+ A single Workers isolate serves many users at once. A module-scoped client is safe **only** while per-user state is passed per call (`options.headers`), not stored on the instance. For browser/SPA usage, the instance may hold the session (`auth.storage`); for SSR, keep the session in an httpOnly cookie and pass `{ headers: { authorization } }` per call (see `cavuno-board-auth`).
66
+
67
+ ```ts
68
+ import { createBoardClient } from '@cavuno/board';
69
+
70
+ // Safe: shared, stateless. Per-user token rides per call.
71
+ export const board = createBoardClient({
72
+ baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
73
+ board: process.env.PUBLIC_CAVUNO_BOARD!,
74
+ });
75
+ ```
76
+
77
+ ## Escape hatch: client.fetch
78
+
79
+ `board.client.fetch<T>(path, init)` is public and first-class — it runs the full pipeline (base path, headers, token, hooks), not a raw `fetch`. Use it to call an endpoint before the SDK ships a typed method for it:
80
+
81
+ ```ts snippet
82
+ const data = await board.client.fetch<{ object: 'list'; data: unknown[] }>(
83
+ '/some-new-endpoint',
84
+ { query: { limit: 5 } },
85
+ );
86
+ ```
87
+
88
+ ## Checklist
89
+
90
+ - [ ] One client created from env values, reused everywhere.
91
+ - [ ] `board` is the `pk_…` key, not the slug.
92
+ - [ ] No per-user state on a shared SSR instance.
93
+ - [ ] Caching done via per-call `FetchOptions`, not a wrapper.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: cavuno-board-errors
3
+ description: Handle errors and access gating with the @cavuno/board SDK — the BoardApiError shape, the typed guards (isNotFound, isUnauthorized, isValidationError, isRateLimited, isForbidden, isConflict), and the board-password flow (isBoardPasswordRequired → password.verify → X-Board-Access grant).
4
+ ---
5
+
6
+ # Errors and access gating
7
+
8
+ Every SDK method throws on a non-2xx response. The error keeps the server's full typed envelope — never string-match messages.
9
+
10
+ ## When to use
11
+
12
+ - Branching on failures (not found, unauthorized, validation, rate limit).
13
+ - Unlocking a password-protected board.
14
+
15
+ ## The BoardApiError shape
16
+
17
+ ```ts no-check
18
+ class BoardApiError extends Error {
19
+ status: number;
20
+ code: string; // `<domain>_<snake_reason>`
21
+ details?: unknown; // structured, per-code
22
+ requestId?: string;
23
+ raw: unknown; // parsed body, untouched
24
+ }
25
+ ```
26
+
27
+ ## Branch with the typed guards
28
+
29
+ ```ts snippet
30
+ import {
31
+ isBoardApiError,
32
+ isNotFound,
33
+ isUnauthorized,
34
+ isValidationError,
35
+ isRateLimited,
36
+ isForbidden,
37
+ isConflict,
38
+ } from '@cavuno/board';
39
+
40
+ try {
41
+ return await board.jobs.retrieve('senior-chef');
42
+ } catch (err) {
43
+ if (isNotFound(err)) return null; // 404 → render not-found
44
+ if (isUnauthorized(err)) { // 401 → refresh or sign in
45
+ /* see cavuno-board-auth */
46
+ }
47
+ if (isValidationError(err)) { // 400 validation_bad_request → field errors in err.details
48
+ }
49
+ if (isRateLimited(err)) { // 429 → back off
50
+ }
51
+ if (isBoardApiError(err)) {
52
+ console.error(err.code, err.requestId); // log code + requestId for support
53
+ }
54
+ throw err;
55
+ }
56
+ ```
57
+
58
+ `isForbidden` (403) and `isConflict` (409) round out the set. Use `err.requestId` in support reports.
59
+
60
+ ## Password-protected boards
61
+
62
+ A gated board answers reads with `isBoardPasswordRequired` until the visitor presents a grant. Exchange the password once with `password.verify()`; the SDK stores the grant and attaches it as `X-Board-Access` on every subsequent read automatically.
63
+
64
+ ```ts snippet
65
+ import { isBoardPasswordRequired } from '@cavuno/board';
66
+
67
+ try {
68
+ await board.jobs.list({ limit: 20 });
69
+ } catch (err) {
70
+ if (isBoardPasswordRequired(err)) {
71
+ await board.password.verify(userEnteredPassword); // stores the grant
72
+ await board.jobs.list({ limit: 20 }); // now passes the wall
73
+ } else {
74
+ throw err;
75
+ }
76
+ }
77
+ ```
78
+
79
+ The grant is identical for every visitor of the board and is not a user session. On SSR (`nostore`), pass it per call as `{ headers: { 'x-board-access': grant } }` instead of relying on stored state.
80
+
81
+ ## Checklist
82
+
83
+ - [ ] Failures branched via guards, never message string-matching.
84
+ - [ ] `404` → handled not-found path (not a crash).
85
+ - [ ] `err.requestId` logged for support.
86
+ - [ ] Password-gated boards call `password.verify()` on `isBoardPasswordRequired`.
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: cavuno-board-jobs
3
+ description: Browse, search, and render jobs with the @cavuno/board SDK — jobs.list, jobs.search, jobs.retrieve, jobs.similar. Covers the slim card vs full job shapes, storefront pagination (count/limit/offset + opaque cursor), filters, and the candidate-paywall gatedCount.
4
+ ---
5
+
6
+ # Jobs: browse, search, detail
7
+
8
+ The highest-traffic surface. Listing and search return slim `PublicJobCard`s; the detail endpoint returns the full `PublicJob`.
9
+
10
+ ## When to use
11
+
12
+ - Listing/search/keyword/location pages.
13
+ - The job-detail page and its "similar jobs" rail.
14
+
15
+ ## When not to use
16
+
17
+ - Company-scoped listings — use `companies.listJobs` (same card shape).
18
+ - The ungated embeddable widget — use `embed.jobs`.
19
+
20
+ ## List and render cards
21
+
22
+ `jobs.list` returns a `JobCardListEnvelope`: storefront pagination fields plus `data: PublicJobCard[]` and optional `relatedSearches`.
23
+
24
+ ```ts snippet
25
+ const page = await board.jobs.list({ limit: 20, seniority: ['senior', 'lead'] });
26
+ page.count; // total matches ("X jobs")
27
+ page.hasMore; // more pages exist
28
+ page.nextCursor; // opaque forward token, or null
29
+ for (const card of page.data) {
30
+ card.title;
31
+ card.company?.name;
32
+ card.links.public; // canonical /companies/:companySlug/jobs/:jobSlug
33
+ }
34
+ ```
35
+
36
+ ### Filters and pagination
37
+
38
+ `JobsListQuery` supports `limit` (1–100), `offset` (takes precedence over `cursor`), `cursor`, and filters: `companyId`, `remoteOption`, `employmentType`, `seniority` (single or repeated → OR-matched), `location` + `radius` (km), `category`, `skill`. Paginate by passing back `nextCursor`, or use numbered pages with `offset`:
39
+
40
+ ```ts snippet
41
+ const p1 = await board.jobs.list({ limit: 20 });
42
+ const p2 = p1.nextCursor
43
+ ? await board.jobs.list({ limit: 20, cursor: p1.nextCursor })
44
+ : null;
45
+ // or numbered pages:
46
+ const page3 = await board.jobs.list({ limit: 20, offset: 40 });
47
+ ```
48
+
49
+ ## Search
50
+
51
+ `jobs.search` posts a `JobsSearchBody` (free-text `query` + structured `filters`) and returns a `JobCardSearchEnvelope`:
52
+
53
+ ```ts snippet
54
+ const results = await board.jobs.search({
55
+ query: 'chef',
56
+ filters: {
57
+ seniority: ['senior'],
58
+ remoteOption: ['remote'],
59
+ publishedAt: { gte: '2026-01-01T00:00:00Z' },
60
+ },
61
+ limit: 20,
62
+ });
63
+ ```
64
+
65
+ ## Detail and similar
66
+
67
+ ```ts snippet
68
+ const job = await board.jobs.retrieve('senior-chef'); // full PublicJob
69
+ job.description; // HTML
70
+ job.officeLocations;
71
+ job.company?.slug;
72
+ const rail = await board.jobs.similar('senior-chef', { limit: 5 });
73
+ ```
74
+
75
+ ## The candidate paywall: gatedCount
76
+
77
+ On gated boards, some results are hidden from anonymous/unentitled viewers. `gatedCount` is how many were withheld for the current viewer (absent/0 when entitled). Surface it as an upsell rather than pretending the list is complete:
78
+
79
+ ```ts snippet
80
+ const page = await board.jobs.list({ limit: 20 });
81
+ if (page.gatedCount && page.gatedCount > 0) {
82
+ // e.g. "Sign in to see N more roles"
83
+ }
84
+ ```
85
+
86
+ A board-user bearer token on the same call returns the entitled (ungated) view — the endpoint is optional-auth, one URL for both anonymous and personalized reads.
87
+
88
+ ## Checklist
89
+
90
+ - [ ] Listing/search use `PublicJobCard`; detail uses `PublicJob`.
91
+ - [ ] Job links use `links.public` (canonical `/companies/:companySlug/jobs/:jobSlug`).
92
+ - [ ] Pagination via `nextCursor` or `offset`, not client-side slicing.
93
+ - [ ] `gatedCount` surfaced as an upsell, not hidden.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: cavuno-board-setup
3
+ description: End-to-end orchestrator for building a headless Cavuno job board with the @cavuno/board SDK. Start here after `npx @cavuno/board setup` copies the skills — detect the framework, wire the client, render board context, jobs browsing and detail, board-user auth and saved jobs, handle errors and access gating, then verify.
4
+ ---
5
+
6
+ # Setting up a Cavuno board
7
+
8
+ `@cavuno/board` is a thin, isomorphic, typed client for the Cavuno Board API (`/v1/boards/:identifier/*`). It brings the commerce of a job board — jobs, companies, blog, search, auth, saved jobs, alerts — to the framework you already use. You bring the framework and own the layout; the SDK brings the data contract.
9
+
10
+ This skill is the orchestrator. Work top to bottom, delegating each surface to its focused skill.
11
+
12
+ ## When to use
13
+
14
+ - Standing up a new headless board frontend against the Board API.
15
+ - Adding board data (jobs/companies/blog/auth) to an existing app.
16
+ - After `npx @cavuno/board setup` has installed the package and copied these skills.
17
+
18
+ ## When not to use
19
+
20
+ - Authoring the hosted board inside the Cavuno admin (that's the operator Puck builder, not the SDK).
21
+ - Building the operator/admin REST API client — that's `@kit/api-client`, not `@cavuno/board`.
22
+
23
+ ## Inspect the app
24
+
25
+ Read `package.json` and the project layout before writing anything. Identify: the framework (see below), whether it renders on a server (SSR/RSC) or only in the browser, and where server-only secrets are read. Match the project's existing conventions — do not introduce a new data-fetching style.
26
+
27
+ ## Detect the framework
28
+
29
+ `@cavuno/board` is framework-agnostic: it needs only `fetch` and runs in the browser, Node ≥ 20, and Cloudflare Workers. Detect the framework from dependencies and adapt:
30
+
31
+ - `@tanstack/react-start` → the reference flavor. Read `cavuno-board-tanstack-start` for SSR-loader + cookie wiring.
32
+ - `next` → use Server Components + per-call `FetchOptions` (`next: { revalidate, tags }`).
33
+ - Anything else (Nuxt, SvelteKit, Astro, SolidStart, plain JS) → use the core skills directly; the SDK surface is identical.
34
+
35
+ ## Use standard environment names
36
+
37
+ Read these two values from the environment; never hard-code them:
38
+
39
+ - `PUBLIC_CAVUNO_API_URL` — the API base, e.g. `https://api.cavuno.com`.
40
+ - `PUBLIC_CAVUNO_BOARD` — the board identifier. Use the `pk_…` publishable key, not the slug (the slug is operator-mutable and breaks deployed frontends on rename).
41
+
42
+ Both are public-safe (the `pk_…` key is client-safe by design). Use your framework's public-env convention for the variable name (`VITE_`, `PUBLIC_`, `NEXT_PUBLIC_`); the values are the same.
43
+
44
+ ## Keep credentials server-side
45
+
46
+ The board identifier is public. A board-user **bearer JWT is not** — it must never reach the browser bundle. On a server framework, keep the session in an httpOnly cookie owned by your app and pass it per call; see `cavuno-board-auth`.
47
+
48
+ ## Use board route conventions
49
+
50
+ The canonical public job-detail URL is `/companies/:companySlug/jobs/:jobSlug` — a job needs both slugs. `/jobs`, `/jobs/:keyword`, `/jobs/locations/:slug` are listing/search pages, never individual jobs. Mirror these paths so a board migrating hosted → headless keeps its indexed URLs.
51
+
52
+ ## Wire the client
53
+
54
+ Create one client and reuse it. See `cavuno-board-client`.
55
+
56
+ ```ts
57
+ import { createBoardClient } from '@cavuno/board';
58
+
59
+ export const board = createBoardClient({
60
+ baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
61
+ board: process.env.PUBLIC_CAVUNO_BOARD!,
62
+ });
63
+ ```
64
+
65
+ ## Build the board shell from context
66
+
67
+ `board.context()` (a root method on the client) returns identity, theme, analytics, and the board's capability `features` flags. Render branding from it and **gate every optional surface on its capability flag** — only build a route when its feature is enabled. (A dedicated context skill ships with a later slice; the return type is self-describing until then.)
68
+
69
+ ## Build jobs browsing + detail
70
+
71
+ The core surface. `jobs.list` / `jobs.search` for listing pages, `jobs.retrieve` for the detail page, `jobs.similar` for the related rail. Honor storefront pagination and the candidate-paywall `gatedCount`. See `cavuno-board-jobs`.
72
+
73
+ ## Add board users + saved jobs
74
+
75
+ Register/login/refresh/logout, then `me.retrieve` and `me.savedJobs.*`. There is **no auto-refresh on 401** — handle it explicitly. See `cavuno-board-auth`.
76
+
77
+ ## Handle errors + gating
78
+
79
+ Every method throws `BoardApiError` on a non-2xx; branch with the typed guards. Password-protected boards need a `password.verify()` grant. See `cavuno-board-errors`.
80
+
81
+ ## App-owned concerns (out of scope)
82
+
83
+ The SDK serves data only. Your app owns: page layout and chrome copy, marketing/legal prose, and the authoring of SEO artifacts (sitemap, RSS, OG images) — built from API data, not served as documents. The Board API never returns layouts or page-builder JSON.
84
+
85
+ ## Verification
86
+
87
+ - `board.context()` resolves and returns your board's name.
88
+ - A listing page renders cards from `board.jobs.list()`.
89
+ - A detail page renders from `board.jobs.retrieve(slug)`.
90
+ - An invalid slug surfaces a handled `isNotFound(err)` path, not a crash.
91
+
92
+ When the `cavuno-board-smoke-test` skill is present, run it against your `pk_…` to verify end to end.
93
+
94
+ ## Stop conditions
95
+
96
+ Stop and ask the human when: no `pk_…` board identifier or API URL is available; the framework is unrecognized and has no server boundary for secrets; or a surface you need (e.g. job alerts, applications) has no corresponding skill yet — the SDK only exposes endpoints that are live, so a missing skill means the endpoint isn't shipped.
@@ -0,0 +1,102 @@
1
+ ---
2
+ name: cavuno-board-tanstack-start
3
+ description: TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.
4
+ ---
5
+
6
+ # Reference flavor: TanStack Start on Cloudflare Workers
7
+
8
+ This is the framework-specific layer for the reference starter (`wollemiahq/cavuno-board-starter`). The core skills (`cavuno-board-client`, `-jobs`, `-auth`, `-errors`) define the SDK surface; this skill shows how to wire it into TanStack Start on Workers. Read the core skills first.
9
+
10
+ ## When to use
11
+
12
+ - The project depends on `@tanstack/react-start`.
13
+ - You need the SSR-loader + httpOnly-cookie + single-flight-refresh patterns.
14
+
15
+ ## One shared, stateless client
16
+
17
+ ```ts
18
+ import { createBoardClient } from '@cavuno/board';
19
+
20
+ // Module-scoped, no auth.storage → safe across concurrent Workers requests.
21
+ export const board = createBoardClient({
22
+ baseUrl: process.env.PUBLIC_CAVUNO_API_URL!,
23
+ board: process.env.PUBLIC_CAVUNO_BOARD!,
24
+ });
25
+ ```
26
+
27
+ ## Read in server loaders; cache via FetchOptions
28
+
29
+ Call the SDK only on the server (route loaders / server functions), never from the browser with a token. On Workers, pass cache directives straight through:
30
+
31
+ ```ts no-check
32
+ import { createFileRoute } from '@tanstack/react-start';
33
+ import { board } from '~/lib/board';
34
+
35
+ export const Route = createFileRoute('/jobs')({
36
+ loader: async () => {
37
+ // `cf` rides straight to the Workers fetch — the SDK needs no framework knowledge.
38
+ return board.jobs.list({ limit: 20 }, { cf: { cacheTtl: 60 } } as never);
39
+ },
40
+ });
41
+ ```
42
+
43
+ ## Session in an `__Host-` httpOnly cookie owned by the app
44
+
45
+ The SDK never owns the cookie. Store the bearer pair in an `__Host-`-prefixed httpOnly cookie set by your server functions, read it on each request, and pass the token per call:
46
+
47
+ ```ts no-check
48
+ import { board } from '~/lib/board';
49
+ import { readSessionCookie } from '~/lib/session.server';
50
+
51
+ export async function loadMe(request: Request) {
52
+ const { accessToken } = readSessionCookie(request);
53
+ // Pass the token via `options` (2nd arg). The 1st arg is `query`.
54
+ return board.me.retrieve(undefined, {
55
+ headers: { authorization: `Bearer ${accessToken}` },
56
+ });
57
+ }
58
+ ```
59
+
60
+ ## Single-flight refresh
61
+
62
+ Refresh tokens are single-use; concurrent 401s must trigger exactly one rotation. Encode it once and reuse everywhere:
63
+
64
+ ```ts no-check
65
+ import { isUnauthorized } from '@cavuno/board';
66
+ import { board } from '~/lib/board';
67
+
68
+ let inflight: Promise<void> | null = null;
69
+
70
+ // `getAccessToken`/`getRefreshToken` read your __Host- cookie (the SDK never
71
+ // touches it). On a 401 we refresh exactly once — concurrent callers await the
72
+ // same `inflight` promise — then retry `run` with the rotated token. Your
73
+ // refresh handler must write the new pair to the cookie so the retry reads it.
74
+ export async function withFreshSession<T>(
75
+ getAccessToken: () => string,
76
+ getRefreshToken: () => string,
77
+ run: (accessToken: string) => Promise<T>,
78
+ ): Promise<T> {
79
+ try {
80
+ return await run(getAccessToken());
81
+ } catch (err) {
82
+ if (!isUnauthorized(err)) throw err;
83
+ inflight ??= board.auth
84
+ .refresh({ refreshToken: getRefreshToken() })
85
+ .then(() => undefined)
86
+ .finally(() => (inflight = null));
87
+ await inflight;
88
+ return run(getAccessToken()); // retry with the rotated token
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## Canonical URLs
94
+
95
+ Mirror the hosted board's public URLs so indexed links survive a hosted → headless migration: job detail at `/companies/$companySlug/jobs/$jobSlug`, listings at `/jobs`, `/jobs/$keyword`, `/jobs/locations/$slug`.
96
+
97
+ ## Checklist
98
+
99
+ - [ ] All SDK calls are server-side; no bearer token in the browser bundle.
100
+ - [ ] Session in an `__Host-` httpOnly cookie owned by the app, passed per call.
101
+ - [ ] Single-flight refresh wraps concurrent 401s.
102
+ - [ ] Public route paths mirror the hosted board's canonical URLs.
@@ -0,0 +1,47 @@
1
+ {
2
+ "version": "1.3.0",
3
+ "skills": [
4
+ {
5
+ "name": "cavuno-board-auth",
6
+ "description": "Authenticate board users with the @cavuno/board SDK — register, login, refresh, logout, email verification and password reset. Covers bearer-JWT storage modes, the deliberate no-auto-refresh-on-401 rule (and single-flight handling), and the server-side httpOnly-cookie pattern that keeps tokens out of the browser.",
7
+ "path": "skills/cavuno-board-auth/SKILL.md",
8
+ "framework": null,
9
+ "category": "core"
10
+ },
11
+ {
12
+ "name": "cavuno-board-client",
13
+ "description": "Create and configure the @cavuno/board client — baseUrl and the pk_ board identifier, global headers, request/response hooks, per-call FetchOptions caching passthrough, the client.fetch escape hatch, and the rule that keeps one shared instance safe under SSR.",
14
+ "path": "skills/cavuno-board-client/SKILL.md",
15
+ "framework": null,
16
+ "category": "core"
17
+ },
18
+ {
19
+ "name": "cavuno-board-errors",
20
+ "description": "Handle errors and access gating with the @cavuno/board SDK — the BoardApiError shape, the typed guards (isNotFound, isUnauthorized, isValidationError, isRateLimited, isForbidden, isConflict), and the board-password flow (isBoardPasswordRequired → password.verify → X-Board-Access grant).",
21
+ "path": "skills/cavuno-board-errors/SKILL.md",
22
+ "framework": null,
23
+ "category": "core"
24
+ },
25
+ {
26
+ "name": "cavuno-board-jobs",
27
+ "description": "Browse, search, and render jobs with the @cavuno/board SDK — jobs.list, jobs.search, jobs.retrieve, jobs.similar. Covers the slim card vs full job shapes, storefront pagination (count/limit/offset + opaque cursor), filters, and the candidate-paywall gatedCount.",
28
+ "path": "skills/cavuno-board-jobs/SKILL.md",
29
+ "framework": null,
30
+ "category": "core"
31
+ },
32
+ {
33
+ "name": "cavuno-board-setup",
34
+ "description": "End-to-end orchestrator for building a headless Cavuno job board with the @cavuno/board SDK. Start here after `npx @cavuno/board setup` copies the skills — detect the framework, wire the client, render board context, jobs browsing and detail, board-user auth and saved jobs, handle errors and access gating, then verify.",
35
+ "path": "skills/cavuno-board-setup/SKILL.md",
36
+ "framework": null,
37
+ "category": "core"
38
+ },
39
+ {
40
+ "name": "cavuno-board-tanstack-start",
41
+ "description": "TanStack-Start-on-Cloudflare-Workers reference wiring for a headless Cavuno board — SSR loaders calling @cavuno/board server-side, the session held in an __Host- httpOnly cookie owned by the app, a single-flight refresh helper, and FetchOptions cache passthrough on Workers.",
42
+ "path": "skills/flavors/tanstack-start/SKILL.md",
43
+ "framework": "tanstack-start",
44
+ "category": "flavor"
45
+ }
46
+ ]
47
+ }