@escalay/kit 0.0.0-stage → 1.8.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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/cli.mjs +438 -0
  4. package/docs/STANDARD.md +342 -0
  5. package/docs/decisions/0001-features-drizzle-scaffold.md +35 -0
  6. package/docs/decisions/0002-node-and-workers.md +45 -0
  7. package/docs/releasing.md +97 -0
  8. package/package.json +59 -3
  9. package/registry.json +548 -0
  10. package/src/a2a/README.md +25 -0
  11. package/src/a2a/index.ts +262 -0
  12. package/src/boards/README.md +35 -0
  13. package/src/boards/handlers.ts +87 -0
  14. package/src/boards/index.ts +64 -0
  15. package/src/boards/refs.ts +23 -0
  16. package/src/boards/repo.ts +50 -0
  17. package/src/boards/routes.ts +83 -0
  18. package/src/boards/schema.ts +28 -0
  19. package/src/boards/tools.ts +76 -0
  20. package/src/browser/README.md +31 -0
  21. package/src/browser/boards-client.ts +74 -0
  22. package/src/browser/boards-react.ts +73 -0
  23. package/src/browser/search-client.ts +258 -0
  24. package/src/browser/search-react.ts +116 -0
  25. package/src/browser/vector-search-block.tsx +94 -0
  26. package/src/core/README.md +37 -0
  27. package/src/core/api.ts +239 -0
  28. package/src/core/auth.ts +147 -0
  29. package/src/core/cache.ts +53 -0
  30. package/src/core/discovery.ts +315 -0
  31. package/src/core/guards.ts +109 -0
  32. package/src/core/hal.ts +63 -0
  33. package/src/core/kv.ts +29 -0
  34. package/src/core/problem.ts +124 -0
  35. package/src/db/README.md +39 -0
  36. package/src/db/guards.ts +61 -0
  37. package/src/db/index.ts +122 -0
  38. package/src/guide/README.md +38 -0
  39. package/src/guide/guide.ts +152 -0
  40. package/src/guide/reference.ts +204 -0
  41. package/src/index.ts +15 -0
  42. package/src/mcp/README.md +34 -0
  43. package/src/mcp/index.ts +251 -0
  44. package/src/oauth/README.md +29 -0
  45. package/src/oauth/index.ts +439 -0
  46. package/src/oauth/schema.ts +7 -0
  47. package/src/oauth/store.ts +34 -0
  48. package/src/pagination/README.md +31 -0
  49. package/src/pagination/index.ts +65 -0
  50. package/src/testing/README.md +31 -0
  51. package/src/testing/index.ts +162 -0
  52. package/src/vector-search/README.md +27 -0
  53. package/src/vector-search/index.ts +367 -0
  54. package/src/vectors/README.md +30 -0
  55. package/src/vectors/index.ts +326 -0
  56. package/src/web/README.md +21 -0
  57. package/src/web/index.ts +100 -0
  58. package/templates/app/AGENTS.md +41 -0
  59. package/templates/app/CLAUDE.md +3 -0
  60. package/templates/app/Dockerfile +12 -0
  61. package/templates/app/container/entrypoint.sh +7 -0
  62. package/templates/app/gitignore +4 -0
  63. package/templates/app/guide/llms.txt +11 -0
  64. package/templates/app/pnpm-workspace.yaml +5 -0
  65. package/templates/app/scripts/guide.ts +8 -0
  66. package/templates/app/src/app.ts +39 -0
  67. package/templates/app/src/features/index.ts +30 -0
  68. package/templates/app/src/server.ts +14 -0
  69. package/templates/app/test/api.test.ts +34 -0
  70. package/templates/app/tsconfig.json +16 -0
  71. package/templates/feature/feature.test.ts +19 -0
  72. package/templates/feature/handlers.ts +43 -0
  73. package/templates/feature/index.ts +34 -0
  74. package/templates/feature/repo.ts +24 -0
  75. package/templates/feature/routes.ts +43 -0
  76. package/templates/feature/schema.ts +14 -0
  77. package/templates/feature/tools.ts +46 -0
  78. package/templates/npm-release/release-please-config.json +60 -0
  79. package/templates/npm-release/release-please-manifest.json +3 -0
  80. package/templates/runtime-cloudflare/dev.vars.example +3 -0
  81. package/templates/runtime-cloudflare/scripts/migrations.ts +43 -0
  82. package/templates/runtime-cloudflare/src/worker.ts +19 -0
  83. package/templates/runtime-cloudflare/test/workers/api.test.ts +37 -0
  84. package/templates/runtime-cloudflare/test/workers/apply-migrations.ts +5 -0
  85. package/templates/runtime-cloudflare/test/workers/env.d.ts +11 -0
  86. package/templates/runtime-cloudflare/test/workers/key.ts +2 -0
  87. package/templates/runtime-cloudflare/test/workers/tsconfig.json +7 -0
  88. package/templates/runtime-cloudflare/vitest.config.ts +23 -0
  89. package/templates/runtime-cloudflare/wrangler.jsonc +16 -0
@@ -0,0 +1,342 @@
1
+ # The agent-ready API standard
2
+
3
+ How an HTTP API on the kit is built, so an agent that has used one can use them all: it reads `/llms.txt`,
4
+ follows links, branches on error codes, and calls the same operations as MCP tools. The kit implements each rule as
5
+ parts an app copies in (`npx @escalay/kit add`, see the README); its `testing` part enforces them.
6
+
7
+ ## 1. Stack and layout
8
+
9
+ - **Hono + `@hono/zod-openapi`** (zod 4): every route is `createRoute` + `app.openapi`, so validation, types and the
10
+ OpenAPI document come from one definition. `createAgentApi()` wires the rest.
11
+ - **One service, one origin:** `/api/v1/*` (the API), `/mcp` (MCP), `/openapi.json`, `/docs` (Scalar), `/llms.txt`,
12
+ `/llms-full.txt`, `/agents/*.md`, `/skills/*`, `/problems/{code}`, `/api/health`, the discovery documents (§14),
13
+ then media and web pages.
14
+ - **In a repo:** `apps/api/` (`src/features/<name>/` one folder per feature: `schema.ts` tables and migration steps,
15
+ `repo.ts` the only queries, `routes.ts` contracts, `handlers.ts` request → repo → HAL, `tools.ts` MCP tools,
16
+ `index.ts`; `src/app.ts` only wires them; `src/kit/` the copied kit with its `kit.json`, `guide/` the agent guide,
17
+ `test/`), shared logic in `packages/*`. `kit init` makes this layout and `feature` adds to it. The app owns its kit copy: edit it when it must differ, and
18
+ `kit diff` shows those edits next to the kit's changes.
19
+ Read-only data an API serves is built elsewhere (a store in object storage) and synced in; the API never parses raw sources.
20
+ - **Two runtimes, one app:** the same `createApp` runs on Node (`server.ts`, a container) and on Cloudflare Workers
21
+ (`worker.ts`, `nodejs_compat`). Nothing runs at global scope that Workers refuse there (random values, I/O, timers),
22
+ and no module imports a Node-only API at the top unless Workers provide it. The kit's tests run in both (`node` and
23
+ `workers` projects); `test/workers/worker.ts` imports the whole kit at global scope so a regression fails to start.
24
+ - **What the API writes goes through Drizzle** (`db`): one `openDb(source)` per app (a `node:sqlite` file on Node; D1
25
+ on Workers, `openDb(drizzle(env.DB))`; Turso by passing its Drizzle database), and only a feature's repo queries it, never a handler. Each feature
26
+ brings its tables and its migration steps (`migrate(db, feature, steps)`, recorded in `kit_migrations`); a step
27
+ never changes once shipped. Read-only catalogs (a built store, JSON in memory) stay behind their own store module.
28
+ - **OAuth is a part of its own** (`oauth`): `createAgentApi({ oauth: createOAuth({ store: await dbOAuthStore(db) }) })`.
29
+
30
+ ## 2. Resources and URLs
31
+
32
+ - Plural nouns, ids from the source system, slugs for taxonomy: `/api/v1/recipes/{id}`, `/api/v1/categories/{slug}`.
33
+ Actions that are not resources are verbs under a noun: `POST /api/v1/menus/check`.
34
+ - Fields are `camelCase`; dates are ISO 8601 (`2026-10-31`, `2026-10-31T12:00:00Z`); money is `{ usd }`.
35
+ - `/api/v1` is a promise: changes are additive. A breaking change is `/api/v2`, with `Deprecation` and `Sunset`
36
+ headers on the old routes for at least a month.
37
+ - `GET /api` (the root) lists every collection with links and live counts: the one URL an agent needs to start.
38
+
39
+ ## 3. Representations: HAL
40
+
41
+ - Media type `application/hal+json` for resources and pages; problems are `application/problem+json`.
42
+ - **`_links`** on every resource: absolute `href`s (built from the forwarded proto and host), `self` always, plus
43
+ every neighbour worth visiting (`cook`, `recipes`, `similar`, `web` for the human page). Agents follow links; they
44
+ never build URLs from ids.
45
+ - **`_embedded`** holds related resources inlined on request (`?include=steps,cook`), each once.
46
+ - Lists and search results carry **summaries** (id, name, a few key fields, `_links.self`); `GET` the link for the
47
+ full resource. `?fields=a,b` trims a resource; heavy parts (full text, steps) are opt-in.
48
+ - A score is a number with its meaning documented (cosine, rank-only); a model label is `{ value, confidence }` or
49
+ `"unsure"` below its measured threshold, never a bare guess.
50
+
51
+ ## 4. Lists
52
+
53
+ - Cursor paging: `?limit=` (documented max) and `?cursor=` copied from `_links.next`; items under
54
+ `_embedded.<plural>`, `count` on the page, `total` only when cheap. Keep reading until `_links.next` is absent.
55
+ - Small in-memory lists may page by `page`/`perPage` with `first`/`prev`/`next`/`last` links; the item shape is the
56
+ same.
57
+ - Filters are plain query parameters; repeating one means OR (`?course=starter&course=main`). An unknown value is a
58
+ `400`, never silently ignored. So is an unknown parameter: `createAgentApi` answers a `400` naming the parameters the
59
+ operation does take (read from its query schema), so `?sinse=` can't pass for a filter. Sorting is `sortBy=` one of
60
+ the documented fields and `order=asc|desc` (`sortQuery`).
61
+
62
+ ## 5. Errors: RFC 9457 problem details
63
+
64
+ Every error is `application/problem+json`: `type` (a URI ending in the code, served at `/problems/{code}`), `code`
65
+ (branch on this), `title`, `status`, `detail` (what happened and what to do next), `instance` (the request id), and
66
+ for `bad_request` an `errors` list of `{ in, path, message }`.
67
+
68
+ | Code | Status | Meaning |
69
+ |---|---|---|
70
+ | `bad_request` | 400 | A parameter or the body is invalid; `errors` lists each one. |
71
+ | `unauthorized` | 401 | No valid API key; `WWW-Authenticate: Bearer realm="…"`. |
72
+ | `forbidden` | 403 | A valid key that may not do this. |
73
+ | `not_found` | 404 | No such route or id in this snapshot. |
74
+ | `conflict` | 409 | Clashes with the current state. |
75
+ | `payload_too_large` | 413 | Over the documented size. |
76
+ | `too_many_requests` | 429 | Wait `Retry-After` seconds. |
77
+ | `spend_cap` | 429 | A paid operation's budget is spent; free ones still work. |
78
+ | `upstream` | 503 | A model or storage call failed; `Retry-After` (30 s by default). Never 502: Cloudflare replaces a 502's body with its own page. |
79
+ | `unavailable` | 503 | Data loading or being replaced; `Retry-After`. A thrown 503 the app didn't name becomes this, never `upstream`. |
80
+ | `internal` | 500 | Our bug; quote `instance`. |
81
+
82
+ ## 6. OpenAPI 3.1
83
+
84
+ `/openapi.json` (`doc31`) and Scalar at `/docs`. The document is the contract, so it must stand on its own:
85
+
86
+ - Every operation: a unique camelCase `operationId` (`listRecipes`, `searchRecipes`), a `summary` ≤ 80 characters, a
87
+ `description` that says when to use it and what comes back, one or more declared `tags`, a description on every
88
+ parameter (with an `example`), a named, described 2xx schema, and its 4xx/5xx as problem+json
89
+ (`...problemResponses(400, 404)`).
90
+ - Every named schema (`.openapi('Recipe', { description })`) is described; fields carry `description` and `example`.
91
+ - `info.description` says what the API is and where to start; `externalDocs` points at `/llms.txt`; `x-tagGroups`
92
+ orders the sidebar; `security` is global with `security: []` on open routes.
93
+ - `lintOpenApi(doc)` in the app's tests must return `[]`.
94
+
95
+ ## 7. Auth
96
+
97
+ - **Agents and scripts:** an API key per client, `Authorization: Bearer <prefix>_<random>`. The service holds only
98
+ hashes (`API_KEYS=laptop:<sha256>,ci:<sha256>`); the keys live in **your secret store, scoped to the app**,
99
+ one secret per client (`API_KEY_LAPTOP`, …). Revoke = remove its line and redeploy.
100
+ - **People:** Cloudflare Access SSO in front of the web pages; the app also verifies `Cf-Access-Jwt-Assertion` against
101
+ the team's keys, so nothing reaches data without either a key or a verified person.
102
+ - **A browser app on the same host:** on the bypassed paths Access sends no header, but the browser still sends Access's
103
+ `CF_Authorization` cookie (the same signed token), and the app accepts it. Writes made with the cookie must come from
104
+ the same site (`Sec-Fetch-Site: same-origin`, else a matching `Origin`), so other sites can't post through a login.
105
+ The host must not bypass Access for people (no IP bypass), or they arrive without a token.
106
+ - **Open without auth:** `/api/health`, `/openapi.json`, `/docs`, `/llms*.txt`, `/agents/*`, `/skills/*`,
107
+ `/problems/*` (no data). In Access, these paths and `/api/*`, `/mcp` bypass SSO; the app's key check
108
+ guards them.
109
+ - A 401 carries `WWW-Authenticate: Bearer realm="<app>"` (plus `error="invalid_token"` for a wrong key). Keys never go
110
+ in URLs or logs.
111
+ - **Agents that get their own token (optional, `oauth.ts`):** the app is its own OAuth server. MCP clients follow the
112
+ 401's `resource_metadata` to `/.well-known/oauth-protected-resource[/mcp]`, register (RFC 7591, public clients), and
113
+ send the owner to `/oauth/authorize` (code + PKCE S256); other agents follow `/auth.md` (anonymous or by email),
114
+ and the owner types the agent's 6-digit code at `/oauth/claim`. Both approval pages sit behind Access and check the
115
+ signed-in person again; **nothing is granted before a person approves** (an unclaimed registration has no scopes).
116
+ Tokens: ES256 JWTs for an hour (`OAUTH_SIGNING_KEY`, a private JWK in your secret store), checked against a revocation list
117
+ on every request; refresh tokens rotate. Scopes: `read` for safe methods, `write` for the rest; the relay endpoints (`/mcp`, `/a2a`,
118
+ `auth.relay`) need only `read`, since each call they make reaches a route that checks its own scope. In Access, only
119
+ `/oauth/authorize` and `/oauth/claim` stay gated; `/oauth/register|token|revoke`, `/agent/identity*`, `/auth.md` and
120
+ `/.well-known/*` bypass it.
121
+
122
+ ## 8. Caching
123
+
124
+ - Data is a snapshot: a GET's ETag is `W/"hash(version, process start, URL, Accept)"`; `If-None-Match` → `304`. The start is in it so a deploy that changes an answer never confirms the old one. On Workers, pass the deploy's version id as `boot` (`snapshotEtag(v, { boot: env.CF_VERSION_METADATA.id })`) so every isolate of one deploy agrees. Writable routes
125
+ (boards) opt out.
126
+ - `Cache-Control`: API `private, no-cache`; media `private, max-age=86400, immutable`; guides and OpenAPI
127
+ `private, max-age=300`; any error `no-store`. **Anything behind auth is `private`, never `public`**: a CDN in front
128
+ (Cloudflare caches `.png`, `.jpg`, `.svg` by extension) would otherwise serve it to anyone. Only discovery files and the
129
+ front door's own page assets are `public`.
130
+
131
+ ## 9. Cost and slow work
132
+
133
+ - A response that spent money says so: `meta.cost: { usd, cached }`. Paid results are cached by input hash, so the
134
+ same request twice costs once.
135
+ - Slow or paid work answers `202 Accepted` with `Location: /api/v1/runs/{id}`; `GET` it until `status` is `done`
136
+ (follow `_links.result`) or `failed`.
137
+ - The guards below keep their state where every instance of the app sees it: memory is right for one Node process;
138
+ on Workers (an isolate each) they use the database (`dbGuardStore(db)`), never KV, which is eventually consistent.
139
+ - Every `POST` and `PATCH` honours `Idempotency-Key` (kept 24 h per caller): a retry gets the first answer
140
+ (`Idempotent-Replayed: true`), the key with another body is a `422`, one in flight a `409`. `/openapi.json` documents
141
+ the header on each of them.
142
+ - Paid routes sit behind `rateLimit` per caller, with the `RateLimit-Policy` / `RateLimit` headers and a `429` with
143
+ `Retry-After` past the limit, so a looping agent can't drain the budget.
144
+ - Every model key is the app's own capped OpenRouter key, never a shared one; the
145
+ cap answers `429 spend_cap`.
146
+
147
+ ## 10. The agent guide
148
+
149
+ Templates in `apps/api/guide/`, served live with `{{origin}}`, `{{snapshot}}`, `{{counts.*}}` filled. On Workers the
150
+ folder is shipped as a module (`kit guide` → `src/guide.gen.ts`, served with `serveGuide({ files })`), and
151
+ a test fails when the module is older than the folder. **Anything the
152
+ code already knows is generated, never written by hand**, so the guide cannot drift from the API:
153
+
154
+ | Placeholder | Generated from |
155
+ |---|---|
156
+ | `{{api.endpoints}}`, `{{api.tag.<slug>}}` | `/openapi.json`: per tag, every operation as a working example call, what it is for, its query parameters (`*` required, enums, defaults), its body type and the schema it returns with its top-level fields |
157
+ | `{{api.unblock}}` | the kit: how to look a call or a field up in the spec, read an error, restart from `/api`, handle a `202` |
158
+ | `{{mcp.tools}}`, `{{mcp.count}}`, `{{mcp.resources}}` | the MCP tools (name, the route its call reaches, description) and resources (URI template, what it gives) |
159
+ | `{{skills.<name>.playbooks}}` | the skill folder: each playbook's title and first sentence |
160
+
161
+ Hand-written: what only a person knows (concepts, trust and measured accuracy, real-id examples, playbook steps and
162
+ judgement). Write it for an agent: an index that points, one source of truth per
163
+ fact, steps with checkable done-when criteria.
164
+
165
+ - **`llms.txt`** ([llmstxt.org](https://llmstxt.org)): H1, a one-paragraph summary, base URL and auth, "Start here"
166
+ with real calls and real ids, then links: Docs, Skills, Optional.
167
+ - **`agents/`**: `concepts.md` (data model, how things link, words to use), `endpoints.md` (every call, filters,
168
+ examples), `trust.md` (what each score and label means, measured accuracy, coverage gaps), `mcp.md` (connecting,
169
+ tools, costs).
170
+ - **`skills/<name>/SKILL.md`** + `references/*.md`: a skill folder (frontmatter `name`, `description`) with one
171
+ playbook per task: when to use, steps with exact calls, done-when, judgement rules, output, cost. Served over HTTP,
172
+ as MCP prompts, and through the MCP Skills extension; copy the folder into the agent's skills folder to install.
173
+ - **`/llms-full.txt`** is assembled from all of them in a fixed order, never written by hand.
174
+ - Tests: `guideRoutes(files, doc)`, `guideGaps(files, vars, { doc, tools })` and `toolRoutes(tools, doc)` return
175
+ `[]`; every example id exists in the data. `toolsRun(tools, app, args, { headers })` runs every tool with sample
176
+ arguments and returns `[]` only when each answers 2xx with a readable summary; a tool with no sample arguments fails
177
+ it, so a new tool can't ship untested.
178
+
179
+ ## 11. MCP
180
+
181
+ - Spec **2026-07-28**, stateless Streamable HTTP at `/mcp` (POST; GET/DELETE → 405), via `@modelcontextprotocol/server`
182
+ v2 `createMcpHandler`; 2025-era clients get the SDK's stateless fallback. Origin checked against the public host.
183
+ 2026-07-28 has no `initialize` (`server/discover` replaced it), but agents open with it out of habit: `mountMcp`
184
+ answers it with the discover facts (capabilities as the server declares them, `listChanged: true`) and a note that it
185
+ isn't needed, never with "method not found".
186
+ - **Tools say their route**: each tool's `tools/list` entry carries `_meta["api-kit/route"]`
187
+ (`GET /api/v1/recipes/{id}`), so a gateway can tell which tool a guide or playbook means when it names a route.
188
+ - **Argument names follow the routes**: a tool's arguments are the route's query and path parameter names (`q`, not
189
+ `query`; `slug` where the path says `{slug}`). Ids also accept the board ref form (`recipe:<slug>`); the route, not
190
+ the tool, resolves it, trying the id as given first, so ids that hold a colon themselves (`step:<slug>`) work.
191
+ - **Tools are thin calls to the routes**, run in-process with the caller's credentials (`inProcess(app)`), so the API
192
+ and MCP cannot drift. Names `snake_case` verbs (`search_recipes`); `title`, a description that says when to use it;
193
+ `annotations` (read-only, idempotent, closed-world unless a tool writes or calls out); `outputSchema` = the route's
194
+ 2xx schema, with its JSON as `structuredContent`; a short text summary first; images as `resource_link`s.
195
+ - Errors are tool results with `isError: true` and the problem's one-line summary, never protocol errors.
196
+ - `instructions` on discover: what the server is for and the order to use tools in (not a repeat of descriptions).
197
+ Cache hints: lists and discover cached an hour, `private`.
198
+ - Skills through the Skills extension (SEP-2640) and as prompts. A stdio wrapper (`overHttp(origin, key)`) runs the
199
+ same tools from a laptop against the remote API.
200
+ - Auth is the API's: the same bearer key, or an OAuth token when the app runs `oauth.ts` (then MCP clients such as
201
+ hosted chat apps connect with no key at all: the 401 on `/mcp` points at `/.well-known/oauth-protected-resource/mcp`).
202
+ Never publish protected-resource metadata without a working authorization server: it sends clients into a flow that
203
+ cannot succeed.
204
+ - `mountMcp` publishes the Server Card (SEP-2127) at `<mcp>/server-card` and `/.well-known/mcp/server-card.json`.
205
+ - **Resources** (`mcpResource`) let a client attach a thing (a recipe with its picture, a tree): a URI template read
206
+ through the routes, JSON as text and images as blobs; a part marked `optional` (an image that may be missing) is
207
+ left out when it fails, so the rest still arrives. `resources/read` carries the URI in the `Mcp-Name` header.
208
+ The guide lists them with `{{mcp.resources}}`.
209
+ - **A2A (optional, `a2a.ts`):** A2A 1.0 over the same tools. `mountA2A` serves the Agent Card at
210
+ `/.well-known/agent-card.json` (one `JSONRPC` interface at `/a2a`, the API key and, with `oauth.ts`, the OAuth flow
211
+ as security schemes, one skill per chosen tool) and JSON-RPC `SendMessage`, `GetTask`, `ListTasks`, `CancelTask`.
212
+ A message's text fills the skill's main argument (a `data` part may give all of them; `metadata.skill` picks the
213
+ skill, else the first); the task's artifact is the tool's summary (text) plus the route's JSON (data). A slow route
214
+ leaves the task `TASK_STATE_WORKING` until `GetTask` finds it done. Tasks are kept a day, visible only to their
215
+ caller. Requests must send `A2A-Version: 1.0` (absent means 0.3: `-32009`); streaming, push and the extended card
216
+ answer the spec's errors and the card says they are off. Pick the one-call workflows as skills, not every tool.
217
+
218
+ ## 12. Secrets and deployment
219
+
220
+ - Each app's secrets live in your secret store, scoped to that app (`API_KEY_<CLIENT>` values, `API_KEYS` hashes,
221
+ capped model keys, a read-only storage key). Values never enter a repo, doc, commit or transcript.
222
+ - On your host as a container: the API runs with an allowlisted env (`container/entrypoint.sh`), behind Cloudflare
223
+ Access with the bypass paths above; a separate job syncs any read-only store in.
224
+ - **Or on Cloudflare Workers** (`init --runtime cloudflare|both`): `wrangler.jsonc` pins `account_id` (the global
225
+ wrangler login may be another account), D1 is the database, secrets go in with `wrangler secret put` (locally
226
+ `.dev.vars`), and `pnpm run deploy` (never `pnpm deploy`, pnpm's own command) writes the guide module and the D1 migration files, applies them, then deploys.
227
+ Deploy with a scoped token kept in the app's secrets.
228
+
229
+ ## 13. Checklist for a new API
230
+
231
+ 1. `createAgentApi({ title, version, description, tags, tagGroups, auth })`.
232
+ 2. Routes with `createRoute`: HAL responses, `problemResponses`, cursor pages, real `example`s; every accepted query
233
+ parameter in the route's query schema (anything else is a `400`).
234
+ 3. `GET /api` root with links and counts.
235
+ 4. `guide/` with `llms.txt`, `agents/` and a skill folder, the code-derived parts as generated placeholders;
236
+ `serveGuide`.
237
+ 5. MCP tools over the routes; `mountMcp`; a stdio entry; `serveAiCatalog` last.
238
+ 6. Tests: `lintOpenApi`, `guideRoutes`, `guideGaps` (with the doc and tools), `toolRoutes`, `toolsRun` (every tool, run), contract tests per route,
239
+ an MCP round trip, and one agent given only `/llms.txt` and a key doing a real task (note where it stalls).
240
+ 7. Keys minted per client into your secret store; hashes into `API_KEYS`; Access app with the bypass paths, `/.well-known`
241
+ and `/robots.txt` included.
242
+ 8. Pictures or prose to search by sense: ledgers built with `embedMissing` (pilot, budget, capped key),
243
+ `registerVectorSearch` with the app's filters, `vectorSearchTools`, the measured `minZ` in the trust page (§15).
244
+ 9. A browser app: the front door (`web.ts`), and the headless search parts under its own UI (§16).
245
+
246
+ ## 14. Discovery
247
+
248
+ Public, read-only, generated from what the app already serves (`discovery.ts`); cached an hour with ETags and open
249
+ CORS:
250
+
251
+ | Where | What |
252
+ |---|---|
253
+ | `/.well-known/api-catalog` | RFC 9727 linkset: OpenAPI (`service-desc`), docs and guide (`service-doc`), health (`status`) |
254
+ | `Link` header on `/`, `/api`, health, `/docs`, `/llms.txt` and every `401` | RFC 8288: `api-catalog`, `service-desc`, `service-doc`, `describedby` |
255
+ | `/robots.txt` | Content Signals (`ai-train`, `search`, `ai-input`; default all `no`), `Disallow: /` unless `crawl`, `Agentmap` |
256
+ | `Accept: text/markdown` on `/` and `/docs` | the guide (`llms.txt`) with `x-markdown-tokens`; browsers still get HTML |
257
+ | `/.well-known/agent-skills/index.json` | Agent Skills Discovery v0.2.0; a skill with references as one deterministic `.tar.gz`, digests over the exact bytes served |
258
+ | `<mcp>/server-card`, `/.well-known/mcp/server-card.json` | MCP Server Card (SEP-2127): identity, the remote with its auth header, protocol versions |
259
+ | `/.well-known/ai-catalog.json` | AI Catalog (ARD): the API, the MCP server and each skill, with representative queries |
260
+ | `/sitemap.xml` (`discovery.sitemap`) | the public pages only, named in robots.txt |
261
+ | `/.well-known/agent-card.json` (`a2a.ts`) | A2A 1.0 Agent Card: the JSON-RPC interface, security schemes, skills |
262
+ | `/.well-known/poppy.json` (`discovery.poppy`) | Personal Agent Protocol 0.3: the organization (this host), its APIs and agent protocols, the issuer with scope hints |
263
+ | with `oauth.ts` | `/.well-known/oauth-protected-resource[/<path>]`, `/.well-known/oauth-authorization-server` (with `agent_auth`, `protected_resources`, `poppy_domains`), `/.well-known/jwks.json`, `/auth.md` |
264
+
265
+ An app with a public page adds WebMCP there itself (tools registered on load); DNS-AID records live in DNS, outside
266
+ the app.
267
+
268
+ **Private services** (`createAgentApi({ private: true })`, `serveFrontDoor(app, { …, private: true })`): for content
269
+ whose existence shouldn't be advertised. Everything above except the OAuth metadata, `jwks.json` and `robots.txt`
270
+ needs credentials; `robots.txt` names nothing (no guide, catalog or sitemap); `/api/health` answers status and
271
+ version only; no `Link` header points at the guide; `/` sends people to the app (behind the sign-in) and the app's
272
+ files need credentials too. Every response carries `X-Robots-Tag: noindex, nofollow, noarchive, nosnippet,
273
+ noimageindex` and every page a `noindex` meta, and `public` cache-control becomes `private` (a CDN that caches by
274
+ extension would otherwise hand an authenticated `/llms.txt` to anyone). MCP clients still connect through the 401's
275
+ resource metadata; agents with a key read the guide as before. Name the OAuth `resource_name` and the auth realm
276
+ generically: both stay public.
277
+
278
+ ## 15. Search by vectors
279
+
280
+ Where items have pictures or prose worth searching by sense, not by keyword (`vectors.ts`, `vector-search.ts`):
281
+
282
+ - **Model and spaces.** Gemini Embedding 2 through the app's own capped OpenRouter key, 768 dimensions, unit vectors.
283
+ `look` = the item's picture (flattened onto white, at most 384 px); `meaning` = its words as
284
+ `title: … | text: …` (`documentText`); queries as `task: search result | query: …`. Text and pictures share the
285
+ space, so words can search looks and a picture can search meanings. Plain fetch for both (the AI SDK embeds strings
286
+ only).
287
+ - **Never pay twice.** One append-only JSONL ledger per space, keyed by item, with the hash of model, size and content;
288
+ `embedMissing` embeds only stale items and appends each batch as it returns. Builds run off the request path (a job,
289
+ or a larger machine for big sets), keep ledgers in object storage, start with a pilot (`limit: 200`) and carry a `budget`; a
290
+ 402/403 is a `SpendCapError` that stops the run, and a rerun resumes.
291
+ - **Routes.** `GET {path}?q=&by=look|meaning|both` (words), `POST {path}` (a picture: multipart `image` or JSON
292
+ `{ image }`, filters in the query), `GET {item}/similar?by=` (likeness, free). Results are HAL,
293
+ `_embedded.results[]` of `{ score, z, space, item }`. Every filter is declared on all three.
294
+ - **Only what stands out.** Words and pictures keep rows `minZ` standard deviations above the query's own mean over
295
+ the whole catalog (default `max(3.5, √(2 ln n) − 0.5)`); measure the bar on each catalog with real and nonsense
296
+ queries and record it in the guide's trust page. A picture always has look-alikes, so it takes its own, lower bar
297
+ (`minZ.picture`, default 2.5). A corpus all about one subject makes everything
298
+ stand out a little: there, add a raw-score floor (`minScore`) measured the same way. In practice z separates real
299
+ queries from gibberish poorly on a topical catalog (gibberish can reach z 6), the raw score well: measure both
300
+ on the full build, then prefer a low `minZ` (≈ 2.5) with a `minScore` between the two groups. `both` merges spaces on
301
+ `z`, never on raw cosine. An empty answer means nothing fits, and carries `_embedded.closest`: the three nearest rows
302
+ below the bar (`z: null`), so an agent can judge for itself and say they are not matches. Likeness always answers
303
+ (`nearest`). Give the route an `example` query the catalog answers well, and declare `offers: ['meaning']` on a
304
+ text-only catalog so `by=look` is neither advertised nor a 503. z is measured over the whole catalog, filters
305
+ applied after: a narrow filter can leave few rows above a z bar, one more reason the score floor carries the bar.
306
+ - **Pictures by URL**: the API's own image links (fetched in-process with the caller's credentials, so an agent can
307
+ search with a picture the API just showed it), and the app's `imageHosts` (https, exact host, no redirects); data URLs
308
+ and uploads always. A server never fetches an arbitrary URL.
309
+ - **Cost and failure.** Words and pictures cost one embedding each (queries cached in memory), rate-limited per caller
310
+ (`hourlyLimit`, default 600). No key: `503 unavailable`; budget gone: `429 spend_cap`; vectors not built:
311
+ `503` with `Retry-After`. Likeness and every other route keep working.
312
+ - **Agents** get the same routes as MCP tools (`vectorSearchTools`: `search_<noun>`, `search_<noun>_by_image`,
313
+ `similar_<noun>`).
314
+
315
+ ## 16. The browser side
316
+
317
+ - **Front door (`web.ts`).** `/` is a public page with no data (what the service is, how agents connect, WebMCP
318
+ tools if any); the browser app lives under `/app` behind Access (which gates only `/app`, `/oauth/authorize` and `/oauth/claim`);
319
+ the app's old addresses and `/?query` deep links redirect there; `frontDoorOpen` lists what auth leaves open. The
320
+ app's calls carry Access's cookie, so the browser never holds a key.
321
+ - **Headless first.** Search logic for browsers is `search-client.ts` (no framework: a typed client for §15's routes, a
322
+ controller with debouncing and cancellation, paste and drop of pictures) and `search-react.ts` (`useVectorSearch`,
323
+ `usePictureInput`, `useSimilar`). They render nothing; each app draws its own UI.
324
+ - **UI is a suggestion.** `vector-search-block.tsx` is one shadcn block over the hooks (field with a picture chip,
325
+ look/meaning/both, status line, the app's own cards). It uses only `button` and `input`, so Radix and Base UI styles
326
+ both work. Apps install it with `npx shadcn add escalay/api-kit/vector-search-ui` or `kit add vector-search-ui`, then own
327
+ it.
328
+
329
+ ## 17. Boards
330
+
331
+ The owner's own collections of a catalog's items (`boards.ts`, `boards-client.ts`, `boards-react.ts`):
332
+
333
+ - An item is a **ref**, `<type>:<id>`: the type a lowercase word the app chooses, the id everything after the first
334
+ colon (it may hold colons: `segment:episode:intro:2`). Build and read refs with `boardRef` / `parseBoardRef`
335
+ (`refs.ts`, shipped with both `boards` and `boards-client`), never by hand. Boards live in the app's database (`db`) next to
336
+ its OAuth state.
337
+ - `GET /api/boards` (`?item=<ref>`: where it is saved), `POST` (201 + `Location`), `GET|PATCH|DELETE /api/boards/{id}`,
338
+ `PUT|DELETE /api/boards/{id}/items/{ref}` (idempotent; a note per item), and `/suggestions` when the app can say
339
+ what is alike (e.g. the centroid of the board's look vectors). Writes need `write`.
340
+ - Agents get `list_boards`, `create_board`, `get_board`, `save_to_board`, `remove_from_board` (and
341
+ `board_suggestions`). Browsers use the headless client and hooks; the UI is the app's.
342
+
@@ -0,0 +1,35 @@
1
+ # 0001 Feature folders, Drizzle for write state, a scaffolder
2
+
3
+ Status: accepted · Released: v0.10.0 (toolsRun, refs, --web-dir), v0.11.0 (db, Drizzle, oauth part), v1.0.0 (folders, init, feature)
4
+
5
+ ## Context
6
+
7
+ The kit's stores (boards, OAuth) and each app's state held raw `node:sqlite` queries next to the HTTP handlers, so
8
+ moving off a local SQLite file meant rewriting queries in every app, and apps grew one large `api/index.ts`. Two bugs
9
+ came from things defined or tested nowhere: the board ref format (`type:id`, copied by hand) and MCP tools that
10
+ pointed at a real route but failed when run.
11
+
12
+ ## Decisions
13
+
14
+ 1. **Feature folders, one shape.** Kit and apps: `features/<name>/` with `schema.ts` (Drizzle tables + zod types),
15
+ `repo.ts` (the only file with queries), `routes.ts` (createRoute contracts), `handlers.ts` (request → repo →
16
+ HAL, errors → problems), `tools.ts` (MCP tools over the routes), `index.ts` (`registerX(app, { db })`).
17
+ 2. **Drizzle for write state, not catalogs.** Boards, OAuth and every app's state tables go through Drizzle repos.
18
+ Read-only catalogs (a built SQLite store, JSON in memory) stay behind their own store modules.
19
+ 3. **SQLite family now.** One schema serves `node:sqlite` files, Turso/libSQL and D1: `openDb` opens a file itself and
20
+ takes any other Drizzle SQLite database as it is (Turso via `drizzle-orm/libsql`, D1 via `drizzle(env.DB)`).
21
+ Postgres comes later as a second `schema.ts` per feature; repos stay unchanged, and SQLite-only SQL lives inside
22
+ repo functions.
23
+ 4. **Driver: drizzle-orm 0.45 (stable) `sqlite-proxy` over `node:sqlite`.** No native module, so container builds
24
+ need no build tools. The stable line has no `node:sqlite` driver.
25
+ 5. **Existing data is adopted, not recreated.** A table's first migration step matches the table that already exists
26
+ exactly, so live rows come through. An app whose boards also list other things keeps its own `features/boards`
27
+ in this shape, with `parseBoardRef`, instead of `registerBoards`.
28
+ 6. **Kit housekeeping:** `toolsRun` in `testing` (every tool run against its route), `boardRef`/`parseBoardRef`
29
+ (`refs.ts`) in `boards` and `boards-client`, OAuth out of `core` as its own part, one `kit.json` per app for
30
+ server and browser files, `kit init` and `kit feature` generators.
31
+
32
+ ## Consequences
33
+
34
+ - v1.0 changed import paths (`./kit/boards/index.js`): each app moves once.
35
+ - Queries are async (the proxy driver), so repo functions return promises.
@@ -0,0 +1,45 @@
1
+ # 0002 One kit, two runtimes: Node and Cloudflare Workers
2
+
3
+ Status: accepted · Released: v1.2.0 (imports and runs on workerd; D1 migrations; workers test project), v1.3.0 (guard state in the database), v1.4.0 (guide from a module), v1.5.0 (`init --runtime`), v1.5.1 (scaffold fixes), v1.5.2 (D1 migration files replay over a `migrate()`-made database)
4
+
5
+ ## Context
6
+
7
+ New services start on Cloudflare Workers. The
8
+ kit ran only on Node: `db` imported `node:sqlite` at the top, `cache` made a random id at global scope (Workers refuse
9
+ it, so importing the kit stopped a Worker starting), and `migrate` opened transactions with `begin`, which D1 refuses.
10
+ Drizzle was adopted (0001) so the database could move without the features changing; this uses that.
11
+
12
+ ## Decisions
13
+
14
+ 1. **Same layers on both runtimes.** Features, repos, routes, handlers, tools and `createApp` don't change. Only the
15
+ entry differs: `server.ts` (Node, a container) or `worker.ts` (Workers), each opening the database and calling
16
+ `createApp`.
17
+ 2. **Keep `node:crypto`, `node:zlib`, `Buffer`.** Workers provide them under `nodejs_compat` (on by default from
18
+ compatibility date 2026-08-04), and `hashOf` / `hashApiKey` must keep producing the bytes already paid for and
19
+ stored. Moving to `crypto.subtle` would make them async and change every app.
20
+ 3. **Nothing at global scope that Workers refuse.** Random values, I/O and timers happen on first use
21
+ (`snapshotEtag`'s start id); `snapshotEtag` takes `boot` so a deploy's isolates share one ETag.
22
+ 4. **D1 through Drizzle, migrations as batches.** `openDb(drizzle(env.DB))`; `migrate` sends each step and its
23
+ `kit_migrations` record as one D1 batch (atomic). Durable Object SQLite is not a `Db` (its driver is sync) and
24
+ holds no feature data.
25
+ 5. **Row shapes:** raw `db.all(sql…)` rows are arrays on node:sqlite and objects on D1; repos use the query builder or
26
+ name their columns.
27
+ 6. **One suite, two projects.** Vitest `node` (every test) and `workers` (`@cloudflare/vitest-plugin`, inside
28
+ workerd). `test/workers/worker.ts` imports the whole kit at global scope and serves a probe app over D1, so a
29
+ regression fails to start rather than failing in production.
30
+ 7. **Shared state lives in the database (v1.3).** Idempotency and rate limits keep state in a `GuardStore`;
31
+ `dbGuardStore(db)` makes every check one atomic SQLite statement, so all isolates agree. Not KV (eventually
32
+ consistent), not the Rate Limiting binding (10 s or 60 s windows only). A2A's tasks go in a database store too
33
+ (`dbOAuthStore(db)` fits); on Workers it warns when left in memory.
34
+ 8. **The guide ships as a module on Workers (v1.4).** `kit guide` writes the folder as `GUIDE_FILES`;
35
+ `serveGuide({ files })` serves it exactly as from the folder. The folder stays the source.
36
+ 9. **`init --runtime both|cloudflare|node` (v1.5), `both` by default.** One `app.ts`; Node adds `server.ts` and the
37
+ container, Workers add `worker.ts`, `wrangler.jsonc` and a workerd test project. Migrations run at deploy: each
38
+ feature declares its `migrations`, `pnpm migrations` writes one D1 file per step with its `kit_migrations` record
39
+ (refusing to rewrite a shipped step), `wrangler d1 migrations apply` runs them, and `migrate()` at runtime finds
40
+ them done (it stays the fallback for local dev). App scripts use the app's own `tsx` and kit copy, never the network.
41
+
42
+ ## Next (each its own release)
43
+
44
+ v1.6 the front door on Static
45
+ Assets; later a `realtime` module (Durable Objects with hibernating WebSockets, `wait(key, ms)`).
@@ -0,0 +1,97 @@
1
+ # Releasing
2
+
3
+ How an npm package gets from a merged pull request to the registry with no manual step. The same setup works in
4
+ any pnpm repo: `npx shadcn add escalay/api-kit/npm-release` copies it (the workflow, the release-please files, the
5
+ check and this page).
6
+
7
+ ## How a change becomes a release
8
+
9
+ 1. **A pull request with a Conventional Commit title.** CI checks the title, because it becomes the commit.
10
+ 2. **Squash and merge.** The title is now one commit on `main`, and one line of the next release notes.
11
+ 3. **The release pull request.** `release.yml` runs release-please on every push to `main`. It opens, and keeps
12
+ current, one pull request titled `chore(main): release X.Y.Z`: the version bumps and `CHANGELOG.md`.
13
+ `feat` raises the minor version, `fix` and `perf` the patch, a `!` after the type the major.
14
+ 4. **Merge it when you want to release.** That is the only decision a person makes.
15
+ 5. **Publish.** The same workflow tags `vX.Y.Z`, drafts the GitHub release, then typechecks and tests the tagged
16
+ commit, packs every public package, publishes each to npm, waits until npm serves them, runs
17
+ `scripts/release-smoke.sh X.Y.Z` when the repo has one, attaches the tarballs and publishes the GitHub release.
18
+ 6. **Check.** `pnpm release:check` on `main` says, one line each, whether anything is off.
19
+
20
+ ## What the pipeline guarantees
21
+
22
+ - **No npm token exists.** npm trusts this repo's `release.yml` in the `npm` environment (OIDC); nothing to leak
23
+ or rotate.
24
+ - **Provenance.** Every version carries a signed statement of the commit and workflow run that built it.
25
+ - **Tested twice.** The gate runs on the pull request and again on the exact commit that is published.
26
+ - **One version everywhere.** Every public package in the workspace is released together at the manifest's
27
+ version; `workspace:` ranges are written as real versions in the tarballs.
28
+ - **A stranger can install it.** The job fails unless the registry serves each `name@version`, and, with a smoke
29
+ script, unless a fresh install passes.
30
+ - **The GitHub release shows only what shipped.** It stays a draft until npm has the packages, and carries the
31
+ exact tarballs.
32
+
33
+ ## One-time setup for a package repo
34
+
35
+ The repo uses pnpm, has `packageManager` in `package.json`, a `.node-version` whose npm is 11.5.1 or newer
36
+ (Node 24), `typecheck` and `test` scripts, and a `repository.url` in every published `package.json` that points
37
+ at the GitHub repo (provenance checks it).
38
+
39
+ 1. Copy the setup: `npx shadcn add escalay/api-kit/npm-release`, then add
40
+ `"release:check": "node scripts/release-check.mjs"` to the scripts.
41
+ 2. **Publish the first version by hand** (`pnpm publish --access public`). A package must exist on npm before a
42
+ trusted publisher can be added to it.
43
+ 3. Write that version into `.release-please-manifest.json` (`{".": "1.0.0"}`). With more than one public
44
+ package, list each other `package.json` under `extra-files` in `release-please-config.json`
45
+ (`{"type": "json", "path": "packages/<name>/package.json", "jsonpath": "$.version"}`).
46
+ 4. Trust the workflow, once per package (asks for your npm second factor):
47
+ `npm trust github <package> --file release.yml --repo <owner>/<repo> --env npm --allow-publish`
48
+ 5. On GitHub: an environment named `npm` (Settings → Environments; limit it to the `main` branch), and
49
+ Settings → Actions → General → "Allow GitHub Actions to create and approve pull requests".
50
+ 6. Branch rules for `main`: pull requests only, squash merging only with the pull request title as the commit
51
+ message, and the CI checks required. A pull request opened by the workflow's own token does not start CI, so
52
+ on the release pull request the required checks never report: merge it as an administrator, or close and
53
+ reopen it to start them. Its commits were all tested on their own pull requests, and the publish job tests again.
54
+ 7. `pnpm release:check` until every line is ✓.
55
+
56
+ The workflow file must stay named `release.yml`: the trusted publisher names it.
57
+
58
+ ## Titles that read well as release notes
59
+
60
+ The title is read by someone deciding whether to upgrade. Say what changed for them, imperative and lowercase,
61
+ without the ticket, the file or the how.
62
+
63
+ | Good | Bad | Why |
64
+ |---|---|---|
65
+ | `feat: add cursor pages to boards` | `feat: boards changes` | says nothing |
66
+ | `fix: answer 404 for a board that was deleted` | `fix: bug in handlers.ts` | names a file, not a behaviour |
67
+ | `feat(cli)!: drop the --legacy flag` | `refactor: remove flag` | a breaking change hidden from the notes |
68
+ | `perf: embed a batch in one request` | `feat: performance` | wrong type, no content |
69
+ | `docs: explain how to rotate API keys` | `Update README.md` | not a Conventional Commit; CI refuses it |
70
+
71
+ `feat`, `fix`, `perf`, `docs` and `revert` appear in the notes (as New, Fixed, Faster, Documentation, Reverted).
72
+ `refactor`, `chore`, `test`, `build` and `ci` do not, and release nothing on their own.
73
+
74
+ ## When a release fails
75
+
76
+ - **Run the failed jobs again** (Actions → the run → "Re-run failed jobs", not "all jobs": a full re-run finds
77
+ no new release and skips publishing). The publish step skips every `name@version` npm already has.
78
+ - **A version can never be published twice**, even after unpublishing. If a published version is bad, fix it and
79
+ release the next one; `npm deprecate <package>@<version> "<why>"` warns people off the bad one.
80
+ - **"npm still does not serve …" after 20 minutes.** npm can hold a new version back before serving it. The
81
+ package was published; when `npm view <package>@<version>` answers, run the failed jobs again.
82
+ - **`npm publish` answers 404 or "OIDC".** The trusted publisher does not match: `npm trust list <package>` must
83
+ show this repo, `release.yml` and the `npm` environment.
84
+ - **The smoke check fails.** The packages are public and broken for new installs. Fix forward with a `fix:` pull
85
+ request and merge the next release pull request; the GitHub release of the bad version stays a draft.
86
+ - **No release pull request appears.** Only `feat`, `fix`, `perf` and breaking changes raise a version; check the
87
+ repository setting from step 5.
88
+
89
+ ## Checking a published package
90
+
91
+ ```bash
92
+ npm view <package> dist.attestations # the provenance statement's URL and type
93
+ npm audit signatures # in a project that installed it: signatures and attestations verify
94
+ ```
95
+
96
+ On npmjs.com the version shows a "Built and signed on GitHub Actions" badge that links to the commit and the
97
+ workflow run. The same tarballs are attached to the GitHub release.
package/package.json CHANGED
@@ -1,6 +1,62 @@
1
1
  {
2
2
  "name": "@escalay/kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.8.0",
4
+ "type": "module",
5
+ "description": "Agent-ready HTTP APIs as parts an app copies in (shadcn-style): Hono + zod OpenAPI 3.1, HAL links, problem+json, cursors, ETags, API keys, discovery, a generated agent guide with skills, an MCP server over the same routes, vector search by words, picture and likeness, and its headless browser side.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/escalay/api-kit.git"
10
+ },
11
+ "homepage": "https://github.com/escalay/api-kit#readme",
12
+ "bugs": "https://github.com/escalay/api-kit/issues",
13
+ "files": [
14
+ "cli.mjs",
15
+ "registry.json",
16
+ "src",
17
+ "templates",
18
+ "docs"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "engines": {
24
+ "node": ">=22.16.0"
25
+ },
26
+ "devDependencies": {
27
+ "@cloudflare/vitest-plugin": "1.6.0",
28
+ "@cloudflare/workers-types": "5.20261106.1",
29
+ "@hono/node-server": "^2.1.5",
30
+ "@hono/zod-openapi": "^1.6.3",
31
+ "@modelcontextprotocol/server": "^2.4.0",
32
+ "@scalar/hono-api-reference": "^0.12.17",
33
+ "@testing-library/react": "^16.3.3",
34
+ "@types/node": "^24.19.2",
35
+ "@types/react": "^19.3.0",
36
+ "@types/react-dom": "^19.3.0",
37
+ "happy-dom": "^21.0.4",
38
+ "hono": "^4.13.14",
39
+ "jose": "^6.2.14",
40
+ "react": "^19.3.1",
41
+ "react-dom": "^19.3.1",
42
+ "sharp": "^0.34.5",
43
+ "tsx": "4.23.15",
44
+ "typescript": "^7.0.2",
45
+ "vitest": "^5.0.5",
46
+ "wrangler": "4.158.0",
47
+ "zod": "^4.6.7"
48
+ },
49
+ "dependencies": {
50
+ "drizzle-orm": "0.45.4"
51
+ },
52
+ "bin": {
53
+ "kit": "./cli.mjs"
54
+ },
55
+ "scripts": {
56
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.web.json && tsc --noEmit -p test/workers/tsconfig.json",
57
+ "test": "vitest run",
58
+ "test:starter": "node scripts/starter-check.mjs",
59
+ "release:check": "node scripts/release-check.mjs",
60
+ "kit": "node cli.mjs"
61
+ }
6
62
  }