Add an SDK endpoint
Arg skip: record the endpoint as skipped instead of wrapping it.
Run everything from packages/sdk.
Every generated endpoint must either have a call site under src/ or be listed in
src/coverage/skipped.ts. Being in both also fails.
0. Find it
Not in generated/ yet? bun run sync-specs && bun run generate (or ask the user to
run just update-generated if the Rust spec changed locally — it rebuilds apps/web).
just coverage lists UNCOVERED <service>.<endpoint>.
1. Skip or wrap?
With the skip arg, skip. Otherwise ask the user, one line per endpoint, with a
short recommendation — don't decide silently.
Skip-worthy: internal plumbing (auth/session, health, infra, web-app internals, MCP,
batch previews) and features so narrow that no SDK user would reach for them.
Everything a user could plausibly want belongs in the SDK.
To skip: add the method name, alphabetically, to <camelService>Excluded in
src/coverage/skipped.ts, then just coverage && just check. Don't wrap.
To wrap it
- Read the method in
generated/<service>/sdk.gen.ts and its types in
types.gen.ts (note path / query / body).
- Pick a home, and read a sibling first —
entities/tasks/ is the smallest
complete example, entities/documents/document.ts the richest:
- acts on one entity → method on that class
- create/list/search/lookup → the namespace
- new noun → new
src/entities/<noun>/ pair, registered in src/macro.ts
- cross-entity capability → a base in
src/entities/entity.ts
- Never take or return a raw id. Wherever the generated endpoint takes an id,
the SDK takes the entity handle and reads
.id off it internally; wherever it
returns an id, the SDK returns a handle. If the id refers to a noun that has no
entity class yet, build that entity (and its namespace) as part of this change —
do not fall back to a string parameter. The one exception is a static byId,
which is how handles are minted in the first place.
- Match the conventions:
static byId(client, id); detail via Lazy + protected fetch(), exposed with
this.field(...) / this.mappedField(...)
- extend
MacroEntity / FavoritableEntity / PropertiedEntity, setting
entityType / propertyEntityType
- writes touching this entity's detail →
this.mutate(...); others → unwrap(...)
- every generated call goes through
unwrap()
- cursor lists →
paginate() → AsyncGenerator; search → entitySearch(...)
- camelCase +
undefined on the SDK side even when the wire is snake_case/nullable
- TSDoc every public member. No
any.
- New service? Only when the endpoint's service isn't reachable from
MacroClient yet — per service, not per entity; a new entity needs none of this.
Wire the Sdk into src/utils/client.ts, a host into src/config.ts, and an
entry into ACCESSORS in src/coverage/check.ts. That last one is easy to miss:
coverage decides "is this called?" by grepping src/ for the literal text
.<accessor>.<endpoint>(, so a service absent from ACCESSORS reports every
one of its endpoints as UNCOVERED no matter how well you wrapped them. If a
whole service looks uncovered, check that map before believing it.
- Already listed as skipped? Remove it, or coverage fails with
STALE SKIP.
- Verify:
just check && just coverage && bun run lint && bun run format.
- Document: README only for genuinely new user-facing capability. New webhook
events come from the storage spec via
src/events/types.ts — regenerate, never
hand-write.
Never
- Hand-edit
generated/ or specs/ — build output.
- Accept an id where an entity handle belongs.
- Mark something skipped just to make coverage pass.
1---2name: add-sdk-endpoint3description: Wrap a new backend endpoint in the TypeScript SDK (packages/sdk), or record it as skipped. Use when `just coverage` fails, or after adding an endpoint to a Rust service.4---56# Add an SDK endpoint78Arg `skip`: record the endpoint as skipped instead of wrapping it.9Run everything from `packages/sdk`.1011Every generated endpoint must either have a call site under `src/` or be listed in12`src/coverage/skipped.ts`. Being in both also fails.1314## 0. Find it1516Not in `generated/` yet? `bun run sync-specs && bun run generate` (or ask the user to17run `just update-generated` if the Rust spec changed locally — it rebuilds apps/web).1819`just coverage` lists `UNCOVERED <service>.<endpoint>`.2021## 1. Skip or wrap?2223With the `skip` arg, skip. Otherwise **ask the user**, one line per endpoint, with a24short recommendation — don't decide silently.2526Skip-worthy: internal plumbing (auth/session, health, infra, web-app internals, MCP,27batch previews) and features so narrow that no SDK user would reach for them.28Everything a user could plausibly want belongs in the SDK.2930To skip: add the method name, alphabetically, to `<camelService>Excluded` in31`src/coverage/skipped.ts`, then `just coverage && just check`. Don't wrap.3233## To wrap it34351. **Read** the method in `generated/<service>/sdk.gen.ts` and its types in36 `types.gen.ts` (note `path` / `query` / `body`).372. **Pick a home**, and read a sibling first — `entities/tasks/` is the smallest38 complete example, `entities/documents/document.ts` the richest:39 - acts on one entity → method on that class40 - create/list/search/lookup → the namespace41 - new noun → new `src/entities/<noun>/` pair, registered in `src/macro.ts`42 - cross-entity capability → a base in `src/entities/entity.ts`433. **Never take or return a raw id.** Wherever the generated endpoint takes an id,44 the SDK takes the entity handle and reads `.id` off it internally; wherever it45 returns an id, the SDK returns a handle. If the id refers to a noun that has no46 entity class yet, build that entity (and its namespace) as part of this change —47 do not fall back to a `string` parameter. The one exception is a `static byId`,48 which is how handles are minted in the first place.494. **Match the conventions:**50 - `static byId(client, id)`; detail via `Lazy` + `protected fetch()`, exposed with51 `this.field(...)` / `this.mappedField(...)`52 - extend `MacroEntity` / `FavoritableEntity` / `PropertiedEntity`, setting53 `entityType` / `propertyEntityType`54 - writes touching this entity's detail → `this.mutate(...)`; others → `unwrap(...)`55 - every generated call goes through `unwrap()`56 - cursor lists → `paginate()` → `AsyncGenerator`; search → `entitySearch(...)`57 - camelCase + `undefined` on the SDK side even when the wire is snake_case/nullable58 - TSDoc every public member. No `any`.595. **New service?** Only when the endpoint's service isn't reachable from60 `MacroClient` yet — per *service*, not per entity; a new entity needs none of this.61 Wire the `Sdk` into `src/utils/client.ts`, a host into `src/config.ts`, and an62 entry into `ACCESSORS` in `src/coverage/check.ts`. That last one is easy to miss:63 coverage decides "is this called?" by grepping `src/` for the literal text64 `.<accessor>.<endpoint>(`, so a service absent from `ACCESSORS` reports *every*65 one of its endpoints as `UNCOVERED` no matter how well you wrapped them. If a66 whole service looks uncovered, check that map before believing it.676. **Already listed as skipped?** Remove it, or coverage fails with `STALE SKIP`.687. **Verify:** `just check && just coverage && bun run lint && bun run format`.698. **Document:** README only for genuinely new user-facing capability. New webhook70 events come from the storage spec via `src/events/types.ts` — regenerate, never71 hand-write.7273## Never7475- Hand-edit `generated/` or `specs/` — build output.76- Accept an id where an entity handle belongs.77- Mark something skipped just to make coverage pass.