create-theokit 1.25.0 → 1.25.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-theokit",
3
- "version": "1.25.0",
3
+ "version": "1.25.2",
4
4
  "type": "module",
5
5
  "description": "Scaffold a new TheoKit project",
6
6
  "repository": {
@@ -20,7 +20,7 @@ export const policy = 'public'
20
20
 
21
21
  export default AgentBuilder.create()
22
22
  .input(z.object({ instruction: z.string() }))
23
- .model(process.env.LLM_MODEL ?? 'openai/gpt-4o-mini')
23
+ .model(process.env.LLM_MODEL ?? 'openrouter/openai/gpt-4o-mini')
24
24
  .system(
25
25
  'You turn research notes into published output. Read notes with read_notes, then publish. ' +
26
26
  'Publishing is gated: a human approves it before it happens.',
@@ -19,7 +19,7 @@ export const policy = 'public'
19
19
 
20
20
  export default AgentBuilder.create()
21
21
  .input(z.object({ topic: z.string() }))
22
- .model(process.env.LLM_MODEL ?? 'openai/gpt-4o-mini')
22
+ .model(process.env.LLM_MODEL ?? 'openrouter/openai/gpt-4o-mini')
23
23
  .system(
24
24
  'You research a topic and leave notes for the publisher bot. ' +
25
25
  'Write what you find with write_note; read what you already know with read_notes. ' +
@@ -2,4 +2,4 @@
2
2
  OPENROUTER_API_KEY=sk-or-v1-your-key-here
3
3
 
4
4
  # Optional: override the model declared in agents/chat.ts
5
- # LLM_MODEL=openai/gpt-4o-mini
5
+ # LLM_MODEL=openrouter/openai/gpt-4o-mini
@@ -8,13 +8,13 @@ This project includes TheoKit-aware skills that activate automatically when you
8
8
 
9
9
  | Skill | Triggers when editing | What it provides |
10
10
  | ---------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
11
- | theokit-routes | `server/routes/**` | defineRoute API, Zod validation, HTTP methods, dynamic params |
11
+ | theokit-routes | `server/routes/**` | the route() builder, Zod validation, HTTP methods, dynamic params |
12
12
  | theokit-gateways | `server/routes/**`, `**/*webhook*` | handleChannelWebhook, the `@theokit/gateway-*` adapters, signature validation, the onMessage seam |
13
13
  | theokit-agents | `**/*agent*`, `**/*tool*`, `**/*Agent*`, `**/*Tool*` | @Agent, @Tool, @Toolbox decorators, LLM integration |
14
14
  | theokit-database | `**/*schema*`, `**/*db*`, `**/drizzle*`, `**/*migration*`, `**/*seed*` | Drizzle ORM, SQLite, schema patterns, migrations |
15
15
  | theokit-frontend | `app/**` | File-based routing, layouts, theoFetch, useAgent |
16
16
  | theokit-ui | `app/**`, `**/*Chat*`, `**/*Sidebar*`, `**/*Theme*` | @theokit/ui AI components: ChatThread, ChatMessage, ToolCallCard, theming (generic primitives like CodeBlock/Sidebar come from @usetheo/ui) |
17
- | theokit-config | `theo.config*`, `**/*config*` | defineConfig options, plugins, security, storage |
17
+ | theokit-config | `theo.config*`, `**/*config*` | the config() builder, plugins, security, storage |
18
18
 
19
19
  ### Settings
20
20
 
@@ -56,7 +56,7 @@ your environment.
56
56
  ```ts
57
57
  export default AgentBuilder.create()
58
58
  .input(z.object({ message: z.string() }))
59
- .model('openai/gpt-4o-mini')
59
+ .model('openrouter/openai/gpt-4o-mini')
60
60
  .system(BASE_INSTRUCTIONS)
61
61
  .tool(weatherTool)
62
62
  .approval('send_notification', { question: 'Send this notification?' })
@@ -14,9 +14,17 @@ import { weatherTool } from './tools/weather.js'
14
14
  * treats `prompts/ tools/ skills/ lib/ …` as semantic folders, so `agents/tools/weather.ts` never becomes
15
15
  * a `/api/agents/tools/weather` endpoint. Add a second agent as another `agents/<name>.ts`.
16
16
  *
17
- * `@theokit/sdk` runs the agent; conversation turns auto-persist per session. Provider is resolved from
18
- * the environment — OPENROUTER_API_KEY (preferred) OR ANTHROPIC_API_KEY / OPENAI_API_KEY; the model id is
19
- * provider-prefixed so OpenRouter routes it upstream (https://openrouter.ai/models).
17
+ * `@theokit/sdk` runs the agent; conversation turns auto-persist per session.
18
+ *
19
+ * The FIRST segment of the model id picks the provider, and the key it needs follows from that:
20
+ * `openrouter/…` needs `OPENROUTER_API_KEY`, `anthropic/…` needs `ANTHROPIC_API_KEY`, `openai/…`
21
+ * needs `OPENAI_API_KEY`. There is no magic routing — a bare `openai/gpt-4o-mini` goes to OpenAI,
22
+ * not through a gateway, even with an OpenRouter key present. Reaching OpenAI's catalog THROUGH
23
+ * OpenRouter means naming the gateway: `openrouter/openai/gpt-4o-mini`, which is what this file
24
+ * declares, because `.env.example` asks for `OPENROUTER_API_KEY` (one key, many models —
25
+ * https://openrouter.ai/models).
26
+ *
27
+ * Change the id and the key changes with it. Both live in `.env` and here; nothing else to wire.
20
28
  */
21
29
  /**
22
30
  * Who may run this agent, and against which conversation (ADR 0001).
@@ -48,7 +56,7 @@ export default AgentBuilder.create()
48
56
  // so setting it changed the model to exactly what it already was (#398, #408). One expression is
49
57
  // cheaper than an override path through the framework, and it keeps the value visible in the file
50
58
  // that decides it. The literal stays as the fallback: a scaffold has to run with no environment.
51
- .model(process.env.LLM_MODEL ?? 'openai/gpt-4o-mini')
59
+ .model(process.env.LLM_MODEL ?? 'openrouter/openai/gpt-4o-mini')
52
60
  .system(BASE_INSTRUCTIONS)
53
61
  .tool(weatherTool)
54
62
  .tool(currentTimeTool)
@@ -59,7 +59,7 @@ An agent that outgrows one file becomes a folder that co-locates its own composi
59
59
  ```ts
60
60
  export default AgentBuilder.create()
61
61
  .input(z.object({ message: z.string() }))
62
- .model('openai/gpt-4o-mini')
62
+ .model('openrouter/openai/gpt-4o-mini')
63
63
  .system(BASE_INSTRUCTIONS) // agents/prompts/instructions.ts
64
64
  .tool(weatherTool) // agents/tools/weather.ts
65
65
  .tool(currentTimeTool) // agents/tools/current-time.ts
@@ -17,7 +17,7 @@ Common changes, and where they go. See [ARCHITECTURE](./ARCHITECTURE.md) for the
17
17
 
18
18
  ```ts
19
19
  // agents/tools/echo.ts
20
- import { tool } from 'theokit/server'
20
+ import { tool } from 'theokit/server/define'
21
21
  import { z } from 'zod'
22
22
 
23
23
  export const echoTool = tool('echo')
@@ -8,9 +8,17 @@ Configuration lives in `.env` (copy `.env.example`). Nothing here is committed
8
8
  | `ANTHROPIC_API_KEY` | one of these | Use Anthropic directly instead of OpenRouter. |
9
9
  | `OPENAI_API_KEY` | one of these | Use OpenAI directly. |
10
10
 
11
- The agent resolves the key from the environment at runtime (OpenRouter preferred). The model id in
12
- `agents/chat.ts` is provider-prefixed (e.g. `openai/gpt-4o-mini`) so OpenRouter routes it upstream see
13
- <https://openrouter.ai/models>.
11
+ The FIRST segment of the model id in `agents/chat.ts` picks the provider, and that decides which key
12
+ is needed. It is not a hint: `openai/gpt-4o-mini` goes to OpenAI and needs `OPENAI_API_KEY`, even
13
+ with an OpenRouter key present. Reaching another vendor's catalog THROUGH OpenRouter means naming
14
+ the gateway first — `openrouter/openai/gpt-4o-mini`, which is what the scaffold declares, matching
15
+ the key above. See <https://openrouter.ai/models> for the ids OpenRouter serves.
16
+
17
+ | Model id in `agents/chat.ts` | Key it needs |
18
+ | ------------------------------- | -------------------- |
19
+ | `openrouter/openai/gpt-4o-mini` | `OPENROUTER_API_KEY` |
20
+ | `anthropic/claude-sonnet-4-6` | `ANTHROPIC_API_KEY` |
21
+ | `openai/gpt-4o-mini` | `OPENAI_API_KEY` |
14
22
 
15
23
  ```bash
16
24
  cp .env.example .env
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: theokit-agents
3
- description: TheoKit agent/LLM integration — agents/*.ts convention (defineAgent), @Agent decorator (advanced/DI), defineAgentTool, useAgent client hook
3
+ description: TheoKit agent/LLM integration — agents/*.ts convention (AgentBuilder), the tool() builder, capabilities (advanced/DI), useAgent client hook
4
4
  user-invocable: false
5
5
  paths:
6
6
  - '**/*agent*'
@@ -25,7 +25,7 @@ import { z } from 'zod'
25
25
 
26
26
  export default defineAgent({
27
27
  input: z.object({ message: z.string() }),
28
- model: 'openai/gpt-4o-mini',
28
+ model: 'openrouter/openai/gpt-4o-mini',
29
29
  system: 'You are a helpful assistant.',
30
30
  })
31
31
  ```
@@ -33,8 +33,11 @@ export default defineAgent({
33
33
  The endpoint streams the ai-sdk `UIMessageStream` that `useAgent` (client hook) consumes.
34
34
  `@theokit/sdk` runs the agent; conversation turns auto-persist per session — the SDK owns storage.
35
35
 
36
- **Provider resolution:** `OPENROUTER_API_KEY` (preferred routes to many models) OR
37
- `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`. Set one in `.env`.
36
+ **Provider resolution:** the FIRST segment of the model id picks the provider, and that decides
37
+ which key is needed — `openrouter/…` needs `OPENROUTER_API_KEY`, `anthropic/…` needs
38
+ `ANTHROPIC_API_KEY`, `openai/…` needs `OPENAI_API_KEY`. A bare vendor prefix is a selection of that
39
+ vendor, not a hint: reaching OpenAI's catalog through OpenRouter means naming the gateway first
40
+ (`openrouter/openai/gpt-4o-mini`). Set the matching key in `.env`.
38
41
 
39
42
  ## Advanced Surface — @Agent Decorator (DI / class-based)
40
43
 
@@ -46,7 +49,7 @@ When you need dependency injection or composition, build the agent from **capabi
46
49
  import { applyCapabilities, AgentConfigCapability, ModelCapability } from '@theokit/agents'
47
50
 
48
51
  export const assistantAgent = applyCapabilities([
49
- new ModelCapability('openai/gpt-4o-mini'),
52
+ new ModelCapability('openrouter/openai/gpt-4o-mini'),
50
53
  new AgentConfigCapability({
51
54
  systemPrompt: 'You are a helpful assistant.',
52
55
  maxIterations: 5,
@@ -55,32 +58,52 @@ export const assistantAgent = applyCapabilities([
55
58
  // The framework runs the LLM loop via @theokit/sdk.
56
59
  ```
57
60
 
58
- ## Tools — defineAgentTool
61
+ ## Tools — `tool()`
59
62
 
60
- Declare typed tools with `defineAgentTool` (from `theokit/server`) and pass them to
61
- `defineAgent`'s `tools` array.
63
+ Declare a tool with the `tool()` builder from `theokit/server/define`, then chain it onto the agent
64
+ with `.tool(…)`. It is the same API `agents/tools/weather.ts` in this project uses — read that file
65
+ for a working one.
66
+
67
+ ```typescript
68
+ // agents/tools/current-time.ts
69
+ import { tool } from 'theokit/server/define'
70
+ import { z } from 'zod'
71
+
72
+ export const currentTimeTool = tool('current_time')
73
+ .describe('Return the current ISO timestamp')
74
+ .input(z.object({}))
75
+ .execute(async () => new Date().toISOString())
76
+ .build()
77
+ ```
62
78
 
63
79
  ```typescript
64
80
  // agents/chat.ts
65
- import { defineAgent } from '@theokit/agents'
66
- import { defineAgentTool } from 'theokit/server'
81
+ import { AgentBuilder } from '@theokit/agents'
67
82
  import { z } from 'zod'
68
83
 
69
- const currentTimeTool = defineAgentTool({
70
- name: 'current_time',
71
- description: 'Return the current ISO timestamp',
72
- inputSchema: z.object({}),
73
- handler: async () => new Date().toISOString(),
74
- })
84
+ import { currentTimeTool } from './tools/current-time.js'
75
85
 
76
- export default defineAgent({
77
- input: z.object({ message: z.string() }),
78
- model: 'openai/gpt-4o-mini',
79
- system: 'You are a helpful assistant.',
80
- tools: [currentTimeTool],
81
- })
86
+ export default AgentBuilder.create()
87
+ .input(z.object({ message: z.string() }))
88
+ .model('openrouter/openai/gpt-4o-mini')
89
+ .system('You are a helpful assistant.')
90
+ .tool(currentTimeTool)
91
+ .build()
82
92
  ```
83
93
 
94
+ A tool is pure metadata plus a handler: it describes a capability and does local or HTTP work, and
95
+ it NEVER calls an LLM — the agent decides when to invoke it.
96
+
97
+ **Import from `theokit/server/define`, not `theokit/server`.** The umbrella subpath still resolves
98
+ and prints a deprecation warning naming a removal release; every symbol lives under a domain
99
+ subpath (`define`, `auth`, `http`, `security`, …).
100
+
101
+ > **`defineAgentTool` does not exist.** Earlier versions of this skill taught it. The name is still
102
+ > declared in the published `.d.ts`, so an editor will autocomplete it and `tsc` will accept it —
103
+ > and there is no runtime export behind it on any subpath, so the call throws on the first request
104
+ > (usetheokit/theokit#542). If you find it in older code or in a generated snippet, replace it with
105
+ > the `tool()` builder above.
106
+
84
107
  ### Toolbox class (advanced — state + injected dependencies)
85
108
 
86
109
  A toolbox declares its tools as DATA and keeps handlers as ordinary methods, so the class can hold
@@ -174,7 +197,7 @@ Before writing custom tools, check if they already exist:
174
197
  ## Rules
175
198
 
176
199
  - Tool `name` and `description` are ALWAYS explicit — never inferred from method names (G4)
177
- - Tool `input` uses Zod schema — same pattern as defineRoute
200
+ - Tool `.input()` takes a Zod schema — same pattern as `route().body(…)`
178
201
  - `@UseGuards()` works on agents (shared with HTTP pipeline)
179
202
  - `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings)
180
203
  - Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: theokit-config
3
- description: TheoKit configuration — defineConfig, plugins, security, storage, agents, build targets
3
+ description: TheoKit configuration — the config() builder, plugins, security, storage, agents, build targets
4
4
  user-invocable: false
5
5
  paths:
6
6
  - 'theo.config*'
@@ -12,37 +12,39 @@ paths:
12
12
  ## theo.config.ts
13
13
 
14
14
  ```typescript
15
- import { defineConfig } from 'theokit'
16
-
17
- export default defineConfig({
18
- // Basic
19
- name: 'my-app', // DNS-1123 format (lowercase + hyphens)
20
- port: 3000, // Dev + production port
21
-
22
- // SSR (default: false)
23
- ssr: false,
24
-
25
- // Security (defaults are secure)
26
- security: {
27
- csrf: true, // CSRF protection (default: true)
28
- csp: 'report-only', // Content Security Policy
29
- },
30
-
31
- // Agent runtime
32
- agents: {
33
- maxRegistries: 100,
34
- registry: {
35
- maxAgents: 100,
36
- idleTimeoutMs: 30 * 60_000,
15
+ import { config } from 'theokit'
16
+
17
+ export default config()
18
+ .set({
19
+ // Basic
20
+ name: 'my-app', // DNS-1123 format (lowercase + hyphens)
21
+ port: 3000, // Dev + production port
22
+
23
+ // SSR (default: false)
24
+ ssr: false,
25
+
26
+ // Security (defaults are secure)
27
+ security: {
28
+ csrf: true, // CSRF protection (default: true)
29
+ csp: 'report-only', // Content Security Policy
30
+ },
31
+
32
+ // Agent runtime
33
+ agents: {
34
+ maxRegistries: 100,
35
+ registry: {
36
+ maxAgents: 100,
37
+ idleTimeoutMs: 30 * 60_000,
38
+ },
37
39
  },
38
- },
39
40
 
40
- // DevTools overlay (dev only)
41
- devtools: true,
41
+ // DevTools overlay (dev only)
42
+ devtools: true,
42
43
 
43
- // Plugins
44
- plugins: [],
45
- })
44
+ // Plugins
45
+ plugins: [],
46
+ })
47
+ .build()
46
48
  ```
47
49
 
48
50
  ## Common Configuration Patterns
@@ -50,50 +52,58 @@ export default defineConfig({
50
52
  ### Adding CORS
51
53
 
52
54
  ```typescript
53
- import { defineConfig } from 'theokit'
54
-
55
- export default defineConfig({
56
- // CORS is handled by the framework — configure in route-level or globally
57
- security: {
58
- cors: {
59
- origin: ['http://localhost:3000', 'https://myapp.com'],
60
- credentials: true,
55
+ import { config } from 'theokit'
56
+
57
+ export default config()
58
+ .set({
59
+ // CORS is handled by the framework — configure in route-level or globally
60
+ security: {
61
+ cors: {
62
+ origin: ['http://localhost:3000', 'https://myapp.com'],
63
+ credentials: true,
64
+ },
61
65
  },
62
- },
63
- })
66
+ })
67
+ .build()
64
68
  ```
65
69
 
66
70
  ### Storage (Postgres + Redis)
67
71
 
68
72
  ```typescript
69
- export default defineConfig({
70
- storage: {
71
- postgres: [{ url: process.env.DATABASE_URL }],
72
- redis: [{ url: process.env.REDIS_URL }],
73
- },
74
- })
73
+ export default config()
74
+ .set({
75
+ storage: {
76
+ postgres: [{ url: process.env.DATABASE_URL }],
77
+ redis: [{ url: process.env.REDIS_URL }],
78
+ },
79
+ })
80
+ .build()
75
81
  ```
76
82
 
77
83
  ### Rate Limiting
78
84
 
79
85
  ```typescript
80
- export default defineConfig({
81
- rateLimit: {
82
- global: { max: 100, windowMs: 60_000 },
83
- },
84
- })
86
+ export default config()
87
+ .set({
88
+ rateLimit: {
89
+ global: { max: 100, windowMs: 60_000 },
90
+ },
91
+ })
92
+ .build()
85
93
  ```
86
94
 
87
95
  ### OpenAPI Generation
88
96
 
89
97
  ```typescript
90
- export default defineConfig({
91
- openapi: {
92
- title: 'My App API',
93
- version: '1.0.0',
94
- outDir: '.theokit',
95
- },
96
- })
98
+ export default config()
99
+ .set({
100
+ openapi: {
101
+ title: 'My App API',
102
+ version: '1.0.0',
103
+ outDir: '.theokit',
104
+ },
105
+ })
106
+ .build()
97
107
  ```
98
108
 
99
109
  ## CLI Commands
@@ -26,7 +26,7 @@ returns the `Response` your route must return.
26
26
 
27
27
  ```typescript
28
28
  import { handleChannelWebhook } from 'theokit/server/agent'
29
- import { telegram } from 'theokit/server/webhook' // also: discord, slack, github, stripe
29
+ import { telegram } from 'theokit/server/webhook' // also: discord, slack, github, stripe, whatsapp
30
30
  import { parseInbound } from '@theokit/gateway-telegram'
31
31
 
32
32
  const response = await handleChannelWebhook(request, new URL(request.url).pathname, {
@@ -42,8 +42,36 @@ const response = await handleChannelWebhook(request, new URL(request.url).pathna
42
42
  The path it expects is `POST /api/agents/<name>/channels/<platform>/webhook`; `<name>` and
43
43
  `<platform>` arrive in `onMessage` as `agent` and `platform`.
44
44
 
45
+ ## Platforms that verify the endpoint first (WhatsApp, Instagram, Messenger)
46
+
47
+ Meta will not deliver anything until it has verified the URL with a `GET` carrying
48
+ `hub.mode=subscribe`, `hub.verify_token` and `hub.challenge`, and it requires the challenge echoed
49
+ back as `text/plain`. Declare a responder per platform and mount the route for `GET` as well as
50
+ `POST`:
51
+
52
+ ```typescript
53
+ import { whatsapp, whatsappSubscribe } from 'theokit/server/webhook'
54
+
55
+ const response = await handleChannelWebhook(request, new URL(request.url).pathname, {
56
+ validators: { whatsapp: whatsapp({ appSecret: process.env.META_APP_SECRET! }) },
57
+ subscribe: { whatsapp: whatsappSubscribe({ verifyToken: process.env.META_VERIFY_TOKEN! }) },
58
+ onMessage: async ({ payload }) => {
59
+ /* … */
60
+ },
61
+ })
62
+ ```
63
+
64
+ `appSecret` is the Meta **app secret**, not the access token — the signature is HMAC-SHA256 of it
65
+ over the raw body. `verifyToken` is the string you typed into the Meta app when registering the URL;
66
+ comparing it is the only thing standing between an arbitrary caller and a subscription. A `GET` for
67
+ a platform with no `subscribe` entry answers `405`, not `404`: the platform is configured, it just
68
+ does not do handshakes.
69
+
70
+ Developing against any of this needs a public URL. `theo.config.ts` has `allowedHosts` for exactly
71
+ that — see the framework README.
72
+
45
73
  **Give it a `Request` whose body has not been read.** It calls `request.json()` itself, so a wrapper
46
- that has already parsed the body — `defineRoute` offers a parsed `body` in its handler context —
74
+ that has already parsed the body — `route().body(…)` offers a parsed `body` in its handler context —
47
75
  leaves nothing for it to read. Mount it where you still hold the original request, or pass a clone.
48
76
 
49
77
  `ChannelMessage` is `{ agent, platform, payload }`. There is no `request` inside `onMessage`, because
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: theokit-routes
3
- description: TheoKit server routes — defineRoute, Zod validation, HTTP methods, dynamic params, error handling
3
+ description: TheoKit server routes — the route() builder, Zod validation, HTTP methods, dynamic params, error handling
4
4
  user-invocable: false
5
5
  paths:
6
6
  - 'server/routes/**'
@@ -9,41 +9,47 @@ paths:
9
9
 
10
10
  # TheoKit Routes
11
11
 
12
- ## defineRoute API
12
+ ## The `route()` builder
13
13
 
14
14
  ```typescript
15
- import { defineRoute } from 'theokit/server/define'
15
+ import { route } from 'theokit/server/define'
16
16
  import { z } from 'zod'
17
17
 
18
- // GET handler — no body, optional params/query
19
- export const GET = defineRoute({
20
- policy: 'public', // who may call it — required
21
- params: z.object({ id: z.coerce.number() }), // URL params
22
- query: z.object({ page: z.coerce.number().optional() }), // Query string
23
- handler: ({ params, query }) => {
24
- return { id: params.id, page: query?.page }
25
- },
26
- })
27
-
28
- // POST handler with body validation + custom status
29
- export const POST = defineRoute({
30
- policy: ({ subject }) => subject !== null, // any authenticated caller
31
- body: z.object({
32
- title: z.string().min(3),
33
- done: z.boolean().default(false),
34
- }),
35
- status: 201,
36
- handler: ({ body }) => {
37
- // body is fully typed from Zod schema
18
+ // GET — no body, optional params/query
19
+ export const GET = route()
20
+ .policy('public') // who may call it — required
21
+ .params(z.object({ id: z.coerce.number() })) // URL params
22
+ .query(z.object({ page: z.coerce.number().optional() })) // query string
23
+ .handler(({ params, query }) => ({ id: params.id, page: query?.page }))
24
+ .build()
25
+
26
+ // POST — body validation + custom status
27
+ export const POST = route()
28
+ .policy(({ subject }) => subject !== null) // any authenticated caller
29
+ .body(
30
+ z.object({
31
+ title: z.string().min(3),
32
+ done: z.boolean().default(false),
33
+ }),
34
+ )
35
+ .status(201)
36
+ .handler(({ body }) => {
37
+ // body is fully typed from the Zod schema
38
38
  return db.insert(tasks).values(body).returning().get()
39
- },
40
- })
41
-
42
- // PUT, DELETE follow the same pattern
43
- export const PUT = defineRoute({ policy: 'public', body: z.object({...}), handler: ({body, params}) => {...} })
44
- export const DELETE = defineRoute({ policy: 'public', params: z.object({id: z.coerce.number()}), handler: ({params}) => {...} })
39
+ })
40
+ .build()
45
41
  ```
46
42
 
43
+ The chain is `.policy()`, `.params()`, `.query()`, `.body()`, `.status()`, `.response()`,
44
+ `.csrf()`, `.handler()`, and `.build()` closes it. `server/routes/health.ts` in this project is a
45
+ working one — read it rather than this block if the two ever disagree.
46
+
47
+ `.csrf(false)` opts a single route out of CSRF enforcement. It is for endpoints that legitimately
48
+ receive third-party POSTs — a Stripe or WhatsApp webhook, an OAuth callback — which authenticate by
49
+ signature rather than by session. `policy('public')` answers a different question (may an
50
+ unauthenticated caller reach this) and does NOT lift the CSRF gate: without `.csrf(false)` a webhook
51
+ is refused `CSRF_INVALID` before its signature is ever checked.
52
+
47
53
  ## policy — who may call this route
48
54
 
49
55
  Required on every exported method. The scanner refuses a route file that omits it and names the
@@ -68,40 +74,40 @@ headers and no cookies: identity arrives as `subject`, established by the transp
68
74
  | `server/routes/tasks/[id].ts` | `/api/tasks/:id` | Dynamic param |
69
75
  | `server/routes/users/[...slug].ts` | `/api/users/*` | Catch-all |
70
76
 
71
- ## defineAction (Server Actions)
77
+ ## `action()` (Server Actions)
72
78
 
73
79
  ```typescript
74
- import { defineAction } from 'theokit/server/define'
80
+ import { action } from 'theokit/server/define'
75
81
  import { z } from 'zod'
76
82
 
77
- export const createTask = defineAction({
78
- input: z.object({ title: z.string() }),
79
- handler: ({ input }) => {
80
- return db.insert(tasks).values(input).returning().get()
81
- },
82
- })
83
+ export const createTask = action()
84
+ .input(z.object({ title: z.string() }))
85
+ .handler(({ input }) => db.insert(tasks).values(input).returning().get())
86
+ .build()
83
87
  ```
84
88
 
89
+ The chain is `.input()`, `.accept()`, `.csrf()`, `.handler()`, and `.build()`.
90
+
85
91
  ## Error Handling
86
92
 
87
93
  ```typescript
88
- import { TheoError } from 'theokit'
94
+ import { TheoError } from 'theokit/server/http'
89
95
 
90
- export const GET = defineRoute({
91
- policy: 'public',
92
- handler: ({ params }) => {
96
+ export const GET = route()
97
+ .policy('public')
98
+ .handler(({ params }) => {
93
99
  const task = db.select().from(tasks).where(eq(tasks.id, params.id)).get()
94
100
  if (!task) throw new TheoError({ code: 'NOT_FOUND', message: 'Task not found' })
95
101
  return task
96
- },
97
- })
102
+ })
103
+ .build()
98
104
  ```
99
105
 
100
106
  Valid error codes: `BAD_REQUEST` (400), `UNAUTHORIZED` (401), `FORBIDDEN` (403), `NOT_FOUND` (404), `CONFLICT` (409), `UNPROCESSABLE_ENTITY` (422), `TOO_MANY_REQUESTS` (429), `INTERNAL_SERVER_ERROR` (500).
101
107
 
102
108
  ## Anti-patterns
103
109
 
104
- - NEVER use `res.status().json()` — use defineRoute with `status:` option
105
- - NEVER parse `req.body` manually — use `body: z.object(...)` in defineRoute
110
+ - NEVER use `res.status().json()` — use `.status(201)` on the `route()` chain
111
+ - NEVER parse `req.body` manually — use `.body(z.object({ … }))` on the chain
106
112
  - NEVER create routes outside `server/routes/` — they won't be discovered
107
113
  - NEVER export non-HTTP-method names — only `GET`, `POST`, `PUT`, `DELETE`, `PATCH`
@@ -16,7 +16,7 @@
16
16
  "typecheck": "tsc --noEmit"
17
17
  },
18
18
  "dependencies": {
19
- "theokit": "^0.56.0",
19
+ "theokit": "^0.60.0",
20
20
  "@theokit/agents": "^10.1.0",
21
21
  "@theokit/sdk": "^4.52.1",
22
22
  "@theokit/ui": "^1.1.0",