create-theokit 1.23.8 → 1.23.11

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 (30) hide show
  1. package/dist/cli.js +55 -44
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +2 -1
  4. package/templates/default/.env.example +1 -1
  5. package/templates/default/CLAUDE.md +9 -8
  6. package/templates/default/_gitignore +5 -2
  7. package/templates/default/_prettierignore +13 -0
  8. package/templates/default/agents/chat.ts +29 -1
  9. package/templates/default/app/components/ChatPanel.tsx +3 -2
  10. package/templates/default/app/hooks/use-transcript.ts +9 -3
  11. package/templates/default/app/layout.tsx +8 -1
  12. package/templates/default/app/lib/constants.ts +2 -1
  13. package/templates/default/app/lib/renderable.ts +78 -0
  14. package/templates/default/app/page.test.tsx +9 -3
  15. package/templates/default/docs/ARCHITECTURE.md +35 -29
  16. package/templates/default/docs/CUSTOMIZATION.md +10 -10
  17. package/templates/default/docs/ENVIRONMENT.md +4 -4
  18. package/templates/default/dot-claude/rules/theokit-conventions.md +2 -0
  19. package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +12 -12
  20. package/templates/default/dot-claude/skills/theokit-config/SKILL.md +6 -6
  21. package/templates/default/dot-claude/skills/theokit-database/SKILL.md +15 -10
  22. package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +11 -11
  23. package/templates/default/dot-claude/skills/theokit-gateways/SKILL.md +71 -0
  24. package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +28 -10
  25. package/templates/default/dot-claude/skills/theokit-ui/SKILL.md +26 -24
  26. package/templates/default/eslint.config.mjs +8 -1
  27. package/templates/default/package.json.tmpl +2 -8
  28. package/templates/default/pnpm-workspace.yaml +21 -0
  29. package/templates/default/server/routes/health.ts +3 -0
  30. package/templates/surfaces/desktop/sidecar/sidecar.ts +6 -2
@@ -1,7 +1,10 @@
1
1
  // @vitest-environment jsdom
2
2
  import { render, screen } from '@testing-library/react'
3
3
  import { describe, it, expect } from 'vitest'
4
- import { ChatThread, ChatMessage, type UIMessage } from '@theokit/ui'
4
+ import { ChatThread, ChatMessage } from '@theokit/ui'
5
+ import { type UIMessage } from 'theokit/client'
6
+
7
+ import { toRenderable } from './lib/renderable'
5
8
  import Page from './page'
6
9
 
7
10
  /**
@@ -22,7 +25,10 @@ describe('default chat page', () => {
22
25
  expect(screen.getByLabelText('New chat')).toBeDefined()
23
26
  })
24
27
 
25
- it('ChatMessage accepts a UIMessage and renders its message container (auto-dispatch)', () => {
28
+ it('a streamed message renders through the conversion the app actually uses', () => {
29
+ // Typed as what `useAgent()` RETURNS, not as what `<ChatMessage>` takes. Those are different
30
+ // types on purpose, and a scaffold that conflated them failed its own typecheck twice
31
+ // (usetheokit/theokit#80, #396). `toRenderable` is the seam, and this is what exercises it.
26
32
  const assistant: UIMessage = {
27
33
  id: 'a-0',
28
34
  role: 'assistant',
@@ -30,7 +36,7 @@ describe('default chat page', () => {
30
36
  }
31
37
  const { container } = render(
32
38
  <ChatThread>
33
- <ChatMessage message={assistant} />
39
+ <ChatMessage message={toRenderable(assistant)} />
34
40
  </ChatThread>,
35
41
  )
36
42
  expect(container.querySelector('[data-slot="chat-message"]')).not.toBeNull()
@@ -60,10 +60,10 @@ An agent that outgrows one file becomes a folder that co-locates its own composi
60
60
  export default AgentBuilder.create()
61
61
  .input(z.object({ message: z.string() }))
62
62
  .model('openai/gpt-4o-mini')
63
- .system(BASE_INSTRUCTIONS) // agents/prompts/instructions.ts
64
- .tool(weatherTool) // agents/tools/weather.ts
65
- .tool(currentTimeTool) // agents/tools/current-time.ts
66
- .skills([dailyBriefingSkill]) // agents/skills/daily-briefing.ts
63
+ .system(BASE_INSTRUCTIONS) // agents/prompts/instructions.ts
64
+ .tool(weatherTool) // agents/tools/weather.ts
65
+ .tool(currentTimeTool) // agents/tools/current-time.ts
66
+ .skills([dailyBriefingSkill]) // agents/skills/daily-briefing.ts
67
67
  .build()
68
68
  ```
69
69
 
@@ -77,11 +77,11 @@ loaded by the model on demand via the `skill_read` tool, so long procedures don'
77
77
 
78
78
  The same agent is reached from three interchangeable frontends — only the transport differs:
79
79
 
80
- | Surface | Frontend dir | Transport |
81
- |---------|--------------|-----------|
82
- | web | `app/` | `HttpTransport` (`/api/agents/chat`) |
83
- | tui | `tui/` | `InProcessTransport` |
84
- | desktop | `frontend/` | `ChannelTransport` (`@theokit/tauri`) |
80
+ | Surface | Frontend dir | Transport |
81
+ | ------- | ------------ | ------------------------------------- |
82
+ | web | `app/` | `HttpTransport` (`/api/agents/chat`) |
83
+ | tui | `tui/` | `InProcessTransport` |
84
+ | desktop | `frontend/` | `ChannelTransport` (`@theokit/tauri`) |
85
85
 
86
86
  All three render the same `@theokit/ui` chat and read `shared/agent.ts` for the greeting + model label.
87
87
 
@@ -89,19 +89,19 @@ All three render the same `@theokit/ui` chat and read `shared/agent.ts` for the
89
89
 
90
90
  The web `app/` is organized **type-based**, the layout most React apps grow into:
91
91
 
92
- | Folder | Holds | Example |
93
- |---|---|---|
94
- | `app/` root | the **route surface** — the only files the router serves | `page.tsx`, `layout.tsx`, `error/loading/not-found.tsx` |
95
- | `components/` | presentational UI (flat `.tsx`; Tailwind, no CSS modules) | `Header`, `ChatPanel`, `Composer` |
96
- | `hooks/` | custom hooks — where **state** lives | `use-transcript.ts` |
97
- | `lib/` | app modules / config | `constants.ts` |
92
+ | Folder | Holds | Example |
93
+ | ------------- | --------------------------------------------------------- | ------------------------------------------------------- |
94
+ | `app/` root | the **route surface** — the only files the router serves | `page.tsx`, `layout.tsx`, `error/loading/not-found.tsx` |
95
+ | `components/` | presentational UI (flat `.tsx`; Tailwind, no CSS modules) | `Header`, `ChatPanel`, `Composer` |
96
+ | `hooks/` | custom hooks — where **state** lives | `use-transcript.ts` |
97
+ | `lib/` | app modules / config | `constants.ts` |
98
98
 
99
99
  ### Route files are not components
100
100
 
101
101
  `page` · `layout` · `loading` · `error` · `not-found` are **route conventions**, not components. The
102
102
  router binds them by **name + location**: matched by `^(page|layout|error|loading|not-found)\.(tsx|ts|jsx|js)$`
103
103
  at a route segment (the `app/` root is the `/` route). `loading.tsx` becomes that route's Suspense
104
- fallback, `error.tsx` its error boundary — *because they sit there, with that name*.
104
+ fallback, `error.tsx` its error boundary — _because they sit there, with that name_.
105
105
 
106
106
  So `loading.tsx` looks like a component (it renders a spinner) but it is **not** one — move it into
107
107
  `components/` and the router no longer finds it, and the route loses its loading UI. This is exactly the
@@ -114,12 +114,12 @@ Two rules make the rest work and keep it honest:
114
114
 
115
115
  1. **State in a hook, view in components.** This is the pattern every serious chat frontend converges on
116
116
  (Vercel `ai-chatbot`, the AI SDK docs). `page.tsx` is a thin composition root — it lays out `<ChatPanel>`
117
- + `<Composer>` and pulls transcript state from `useChatTranscript`. The tricky history + in-flight merge
118
- is isolated + unit-tested (`hooks/use-transcript.test.ts`); the components are dumb and prop-driven.
117
+ - `<Composer>` and pulls transcript state from `useChatTranscript`. The tricky history + in-flight merge
118
+ is isolated + unit-tested (`hooks/use-transcript.test.ts`); the components are dumb and prop-driven.
119
119
  2. **Folders aren't routes.** A folder is served only when it holds a `page`/`layout`/… file, so
120
120
  `components/`, `hooks/`, `lib/` are never routes (Next-style colocation). Add `utils/`, `styles/`,
121
121
  `assets/` when you actually have a helper, a global stylesheet, or an image — a scaffold ships the
122
- folders it *uses*, not empty placeholders (YAGNI).
122
+ folders it _uses_, not empty placeholders (YAGNI).
123
123
 
124
124
  The entry point is framework-owned — there is no `main.tsx`/`index.js`. Routes are files (`page.tsx`), not
125
125
  a `pages/` folder you wire by hand: that's the Next.js-style convention TheoKit is built on.
@@ -129,12 +129,12 @@ a `pages/` folder you wire by hand: that's the Next.js-style convention TheoKit
129
129
  Routing is **file-based**: a screen is a folder under `app/` with a `page.tsx`. The folder name is the URL
130
130
  segment; the home screen is the flat `app/page.tsx`.
131
131
 
132
- | You want | Create | Serves |
133
- |---|---|---|
134
- | a `/settings` screen | `app/settings/page.tsx` | `/settings` |
135
- | a nested screen | `app/settings/billing/page.tsx` | `/settings/billing` |
136
- | a dynamic screen | `app/users/[id]/page.tsx` | `/users/:id` (read the param with react-router's `useParams`) |
137
- | a catch-all | `app/docs/[...slug]/page.tsx` | `/docs/*` |
132
+ | You want | Create | Serves |
133
+ | -------------------- | ------------------------------- | ------------------------------------------------------------- |
134
+ | a `/settings` screen | `app/settings/page.tsx` | `/settings` |
135
+ | a nested screen | `app/settings/billing/page.tsx` | `/settings/billing` |
136
+ | a dynamic screen | `app/users/[id]/page.tsx` | `/users/:id` (read the param with react-router's `useParams`) |
137
+ | a catch-all | `app/docs/[...slug]/page.tsx` | `/docs/*` |
138
138
 
139
139
  The one-command way: **`theokit generate page settings`** creates `app/settings/page.tsx` for you. Each
140
140
  screen can have its own `layout.tsx` / `loading.tsx` / `error.tsx` / `not-found.tsx` (the route special
@@ -144,10 +144,16 @@ files, scoped to that segment).
144
144
  `<Outlet/>`), but prefer TheoKit's own client primitives over the raw react-router ones:
145
145
 
146
146
  ```tsx
147
- import { Link } from 'theokit/client' // react-router Link + route prefetch (intent | viewport)
148
- import { useLocation } from 'react-router' // for active-link styling
149
-
150
- <Link to="/settings" prefetch="intent">Settings</Link>
147
+ import { Link } from 'theokit/client' // react-router Link + route prefetch (intent | viewport)
148
+ import { useLocation } from 'react-router' // for active-link styling
149
+
150
+ export function SettingsLink() {
151
+ return (
152
+ <Link to="/settings" prefetch="intent">
153
+ Settings
154
+ </Link>
155
+ )
156
+ }
151
157
  ```
152
158
 
153
159
  TheoKit's `theokit/client` gives you the Next-parity building blocks: **`Link`** (prefetch), **`Metadata`**
@@ -2,16 +2,16 @@
2
2
 
3
3
  Common changes, and where they go. See [ARCHITECTURE](./ARCHITECTURE.md) for the full layout.
4
4
 
5
- | I want to… | Edit |
6
- |------------|------|
7
- | Change the model | `agents/chat.ts` (`.model(...)`) + the label in `shared/agent.ts` |
8
- | Change the persona / rules | `agents/prompts/instructions.ts` |
9
- | Add a tool (an action the agent can take) | new `agents/tools/<name>.ts`, then `.tool(<name>Tool)` in `agents/chat.ts` |
10
- | Add a skill (a procedure the model loads on demand) | `Skill.create(...)` in `agents/skills/<name>.ts`, then add it to `.skills([...])` in `agents/chat.ts` |
11
- | Add a second agent | new `agents/<name>.ts` → auto-served at `/api/agents/<name>`, bind with `useAgent('<name>')` |
12
- | Change the greeting / app name | `shared/agent.ts` |
13
- | Add a backend route | `server/routes/<name>.ts` |
14
- | Change the provider key | `.env` — `OPENROUTER_API_KEY` (or `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`). See [ENVIRONMENT](./ENVIRONMENT.md) |
5
+ | I want to… | Edit |
6
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
7
+ | Change the model | `agents/chat.ts` (`.model(...)`) + the label in `shared/agent.ts` |
8
+ | Change the persona / rules | `agents/prompts/instructions.ts` |
9
+ | Add a tool (an action the agent can take) | new `agents/tools/<name>.ts`, then `.tool(<name>Tool)` in `agents/chat.ts` |
10
+ | Add a skill (a procedure the model loads on demand) | `Skill.create(...)` in `agents/skills/<name>.ts`, then add it to `.skills([...])` in `agents/chat.ts` |
11
+ | Add a second agent | new `agents/<name>.ts` → auto-served at `/api/agents/<name>`, bind with `useAgent('<name>')` |
12
+ | Change the greeting / app name | `shared/agent.ts` |
13
+ | Add a backend route | `server/routes/<name>.ts` |
14
+ | Change the provider key | `.env` — `OPENROUTER_API_KEY` (or `ANTHROPIC_API_KEY` / `OPENAI_API_KEY`). See [ENVIRONMENT](./ENVIRONMENT.md) |
15
15
 
16
16
  ## Adding a tool
17
17
 
@@ -2,11 +2,11 @@
2
2
 
3
3
  Configuration lives in `.env` (copy `.env.example`). Nothing here is committed — `.env` is gitignored.
4
4
 
5
- | Variable | Required | What it does |
6
- |----------|----------|--------------|
5
+ | Variable | Required | What it does |
6
+ | -------------------- | ------------ | ----------------------------------------------------------------------------- |
7
7
  | `OPENROUTER_API_KEY` | one of these | Provider key. OpenRouter is the default gateway (many models behind one key). |
8
- | `ANTHROPIC_API_KEY` | one of these | Use Anthropic directly instead of OpenRouter. |
9
- | `OPENAI_API_KEY` | one of these | Use OpenAI directly. |
8
+ | `ANTHROPIC_API_KEY` | one of these | Use Anthropic directly instead of OpenRouter. |
9
+ | `OPENAI_API_KEY` | one of these | Use OpenAI directly. |
10
10
 
11
11
  The agent resolves the key from the environment at runtime (OpenRouter preferred). The model id in
12
12
  `agents/chat.ts` is provider-prefixed (e.g. `openai/gpt-4o-mini`) so OpenRouter routes it upstream — see
@@ -19,6 +19,8 @@
19
19
 
20
20
  - File at `server/routes/tasks/[id].ts` maps to `/api/tasks/:id`
21
21
  - Export HTTP method handlers: `export const GET = defineRoute({...})`
22
+ - Every method declares `policy` — who may call it. `'public'` is a valid answer and an explicit one;
23
+ omitting it fails the build with the file named
22
24
  - Use `params: z.object({...})` for URL params, `body:` for request body
23
25
  - Use `status: 201` for creation responses, not manual `res.status()`
24
26
 
@@ -3,17 +3,17 @@ name: theokit-agents
3
3
  description: TheoKit agent/LLM integration — agents/*.ts convention (defineAgent), @Agent decorator (advanced/DI), defineAgentTool, useAgent client hook
4
4
  user-invocable: false
5
5
  paths:
6
- - "**/*agent*"
7
- - "**/*Agent*"
8
- - "**/*tool*"
9
- - "**/*Tool*"
10
- - "**/*toolbox*"
11
- - "**/*Toolbox*"
6
+ - '**/*agent*'
7
+ - '**/*Agent*'
8
+ - '**/*tool*'
9
+ - '**/*Tool*'
10
+ - '**/*toolbox*'
11
+ - '**/*Toolbox*'
12
12
  ---
13
13
 
14
14
  # TheoKit Agents & Tools
15
15
 
16
- ## Server Surface — agents/*.ts (zero-config convention)
16
+ ## Server Surface — agents/\*.ts (zero-config convention)
17
17
 
18
18
  Create an `agents/<name>.ts` file at the project root. It is automatically served at
19
19
  `POST /api/agents/<name>` (dev + build) with no manual route wiring.
@@ -162,12 +162,12 @@ consumeUIMessageStream(response, (message) => {
162
162
 
163
163
  Before writing custom tools, check if they already exist:
164
164
 
165
- | Package | What it provides | When to use |
166
- |---------|-----------------|-------------|
167
- | `@theokit/sdk` | `Agent.create()`, `Tool.create()` (primitive), `Run.stream()` | Core agent runtime — always installed |
165
+ | Package | What it provides | When to use |
166
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
167
+ | `@theokit/sdk` | `Agent.create()`, `Tool.create()` (primitive), `Run.stream()` | Core agent runtime — always installed |
168
168
  | `@theokit/sdk-tools` | Ready-made tools: `createReadFileTool`, `createWriteFileTool`, `createSearchTextTool`, `createGlobTool`, `createShellTool`, etc. | **Check here FIRST** before writing custom tools for coding agents |
169
- | `@theokit/di-agent` | DI-powered agent with decorator injection | When using dependency injection pattern |
170
- | `@theokit/di` | Core DI container (`@Injectable`, `@Inject`) | When `@theokit/di-agent` needs explicit bindings |
169
+ | `@theokit/di-agent` | DI-powered agent with decorator injection | When using dependency injection pattern |
170
+ | `@theokit/di` | Core DI container (`@Injectable`, `@Inject`) | When `@theokit/di-agent` needs explicit bindings |
171
171
 
172
172
  **`Tool.create()` in `@theokit/sdk` is the primitive API.** For coding agents, `@theokit/sdk-tools` has batteries-included tools that wrap `Tool.create()` with file system access, search, shell execution, etc. Don't reimplement what `sdk-tools` already provides.
173
173
 
@@ -3,8 +3,8 @@ name: theokit-config
3
3
  description: TheoKit configuration — defineConfig, plugins, security, storage, agents, build targets
4
4
  user-invocable: false
5
5
  paths:
6
- - "theo.config*"
7
- - "**/*config*"
6
+ - 'theo.config*'
7
+ - '**/*config*'
8
8
  ---
9
9
 
10
10
  # TheoKit Configuration
@@ -16,16 +16,16 @@ import { defineConfig } from 'theokit'
16
16
 
17
17
  export default defineConfig({
18
18
  // Basic
19
- name: 'my-app', // DNS-1123 format (lowercase + hyphens)
20
- port: 3000, // Dev + production port
19
+ name: 'my-app', // DNS-1123 format (lowercase + hyphens)
20
+ port: 3000, // Dev + production port
21
21
 
22
22
  // SSR (default: false)
23
23
  ssr: false,
24
24
 
25
25
  // Security (defaults are secure)
26
26
  security: {
27
- csrf: true, // CSRF protection (default: true)
28
- csp: 'report-only', // Content Security Policy
27
+ csrf: true, // CSRF protection (default: true)
28
+ csp: 'report-only', // Content Security Policy
29
29
  },
30
30
 
31
31
  // Agent runtime
@@ -3,11 +3,11 @@ name: theokit-database
3
3
  description: TheoKit database — Drizzle ORM, SQLite schema, migrations, seeds, db commands
4
4
  user-invocable: false
5
5
  paths:
6
- - "**/*schema*"
7
- - "**/*db*"
8
- - "**/drizzle*"
9
- - "**/*migration*"
10
- - "**/*seed*"
6
+ - '**/*schema*'
7
+ - '**/*db*'
8
+ - '**/drizzle*'
9
+ - '**/*migration*'
10
+ - '**/*seed*'
11
11
  ---
12
12
 
13
13
  # TheoKit Database (Drizzle + SQLite)
@@ -22,7 +22,9 @@ export const tasks = sqliteTable('tasks', {
22
22
  id: integer('id').primaryKey({ autoIncrement: true }),
23
23
  title: text('title').notNull(),
24
24
  done: integer('done', { mode: 'boolean' }).notNull().default(false),
25
- createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
25
+ createdAt: text('created_at')
26
+ .notNull()
27
+ .default(sql`CURRENT_TIMESTAMP`),
26
28
  })
27
29
 
28
30
  export const users = sqliteTable('users', {
@@ -74,10 +76,13 @@ db.delete(tasks).where(eq(tasks.id, 1)).run()
74
76
  import { db } from './index.js'
75
77
  import { tasks } from './schema.js'
76
78
 
77
- await db.insert(tasks).values([
78
- { title: 'Learn TheoKit', done: false },
79
- { title: 'Build an agent', done: false },
80
- ]).run()
79
+ await db
80
+ .insert(tasks)
81
+ .values([
82
+ { title: 'Learn TheoKit', done: false },
83
+ { title: 'Build an agent', done: false },
84
+ ])
85
+ .run()
81
86
 
82
87
  console.log('Seeded database')
83
88
  ```
@@ -3,22 +3,22 @@ name: theokit-frontend
3
3
  description: TheoKit frontend — file-based routing, layouts, theoFetch typed client, useAgent, React patterns
4
4
  user-invocable: false
5
5
  paths:
6
- - "app/**"
6
+ - 'app/**'
7
7
  ---
8
8
 
9
9
  # TheoKit Frontend (React + File-Based Routing)
10
10
 
11
11
  ## File-Based Routing
12
12
 
13
- | File | URL | Purpose |
14
- |------|-----|---------|
15
- | `app/page.tsx` | `/` | Home page |
16
- | `app/layout.tsx` | (wrapper) | Root layout (wraps all pages) |
17
- | `app/error.tsx` | (error) | Error boundary |
18
- | `app/loading.tsx` | (loading) | Suspense fallback |
19
- | `app/not-found.tsx` | (404) | Not found page |
20
- | `app/about/page.tsx` | `/about` | Nested route |
21
- | `app/tasks/[id]/page.tsx` | `/tasks/:id` | Dynamic route |
13
+ | File | URL | Purpose |
14
+ | ------------------------- | ------------ | ----------------------------- |
15
+ | `app/page.tsx` | `/` | Home page |
16
+ | `app/layout.tsx` | (wrapper) | Root layout (wraps all pages) |
17
+ | `app/error.tsx` | (error) | Error boundary |
18
+ | `app/loading.tsx` | (loading) | Suspense fallback |
19
+ | `app/not-found.tsx` | (404) | Not found page |
20
+ | `app/about/page.tsx` | `/about` | Nested route |
21
+ | `app/tasks/[id]/page.tsx` | `/tasks/:id` | Dynamic route |
22
22
 
23
23
  ## Typed API Client (theoFetch)
24
24
 
@@ -98,7 +98,7 @@ consumeUIMessageStream(response, (message) => {
98
98
  ## Path Aliases
99
99
 
100
100
  ```typescript
101
- import { db } from '@/server/db' // @/ = project root
101
+ import { db } from '@/server/db' // @/ = project root
102
102
  import { tasks } from '@/server/db/schema'
103
103
  ```
104
104
 
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: theokit-gateways
3
+ description: Receiving messages from Telegram, WhatsApp, Slack and other platforms — handleChannelWebhook, the @theokit/gateway-* adapters, signature validation, the onMessage seam
4
+ user-invocable: false
5
+ paths:
6
+ - 'server/routes/**'
7
+ - 'server/channels/**'
8
+ - '**/*gateway*'
9
+ - '**/*webhook*'
10
+ ---
11
+
12
+ # TheoKit Gateways
13
+
14
+ Receiving a message from a messaging platform spans three packages, and each owns one part of it.
15
+
16
+ | Package | Its half |
17
+ | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | `theokit` (here) | The HTTP route and the signature check. `handleChannelWebhook` verifies the signature, parses the body, and hands your app the parsed JSON as `payload: unknown` |
19
+ | `@theokit/gateway-*` | Translating that payload into a canonical event, so your app never re-declares a platform's wire format |
20
+ | `@theokit/sdk` | Not involved in this path today |
21
+
22
+ ## Wiring one
23
+
24
+ `handleChannelWebhook` is not mounted for you. It takes a `Request`, the URL path, and a config, and
25
+ returns the `Response` your route must return.
26
+
27
+ ```typescript
28
+ import { handleChannelWebhook } from 'theokit/server/agent'
29
+ import { telegram } from 'theokit/server/webhook' // also: discord, slack, github, stripe
30
+ import { parseInbound } from '@theokit/gateway-telegram'
31
+
32
+ const response = await handleChannelWebhook(request, new URL(request.url).pathname, {
33
+ validators: { telegram: telegram({ secretToken: process.env.TELEGRAM_SECRET_TOKEN! }) },
34
+ onMessage: async ({ agent, platform, payload }) => {
35
+ const event = parseInbound(payload)
36
+ if (event === null) return // not a message this adapter handles — see below
37
+ // hand `event` to your agent
38
+ },
39
+ })
40
+ ```
41
+
42
+ The path it expects is `POST /api/agents/<name>/channels/<platform>/webhook`; `<name>` and
43
+ `<platform>` arrive in `onMessage` as `agent` and `platform`.
44
+
45
+ **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 —
47
+ leaves nothing for it to read. Mount it where you still hold the original request, or pass a clone.
48
+
49
+ `ChannelMessage` is `{ agent, platform, payload }`. There is no `request` inside `onMessage`, because
50
+ the body was read and the signature checked before it ran.
51
+
52
+ ## Never throw out of `onMessage`
53
+
54
+ `onMessage` is awaited **before** the 200 is built, and `handleChannelWebhook` does not catch around
55
+ it. A throw there means the 200 is never built: mounted in a TheoKit route, the rejection reaches
56
+ that route's error boundary and is answered 500 — the platform sees a failed delivery where it
57
+ expected an acknowledgement.
58
+
59
+ This is why every adapter's translator returns `null` (or `undefined`) for a payload it does not
60
+ recognise rather than throwing. Handle the `null` and answer normally.
61
+
62
+ ## Which adapters go through this seam
63
+
64
+ Only platforms that deliver by **webhook**. Telegram and the SMS providers export `parseInbound`
65
+ under that name; LINE, WhatsApp Cloud and Teams export their translation under their own names, each
66
+ with its own signature — read the one you are using.
67
+
68
+ Adapters whose transport is a long-lived connection — Discord, Slack, Mattermost, Matrix, e-mail,
69
+ and WhatsApp's `web` and `baileys` backends — do **not** go through this seam. They own their
70
+ transport, and running one alongside a TheoKit server needs a process lifecycle this scaffold does
71
+ not set up.
@@ -3,8 +3,8 @@ name: theokit-routes
3
3
  description: TheoKit server routes — defineRoute, Zod validation, HTTP methods, dynamic params, error handling
4
4
  user-invocable: false
5
5
  paths:
6
- - "server/routes/**"
7
- - "server/actions/**"
6
+ - 'server/routes/**'
7
+ - 'server/actions/**'
8
8
  ---
9
9
 
10
10
  # TheoKit Routes
@@ -17,6 +17,7 @@ import { z } from 'zod'
17
17
 
18
18
  // GET handler — no body, optional params/query
19
19
  export const GET = defineRoute({
20
+ policy: 'public', // who may call it — required
20
21
  params: z.object({ id: z.coerce.number() }), // URL params
21
22
  query: z.object({ page: z.coerce.number().optional() }), // Query string
22
23
  handler: ({ params, query }) => {
@@ -26,6 +27,7 @@ export const GET = defineRoute({
26
27
 
27
28
  // POST handler — with body validation + custom status
28
29
  export const POST = defineRoute({
30
+ policy: ({ subject }) => subject !== null, // any authenticated caller
29
31
  body: z.object({
30
32
  title: z.string().min(3),
31
33
  done: z.boolean().default(false),
@@ -38,18 +40,33 @@ export const POST = defineRoute({
38
40
  })
39
41
 
40
42
  // PUT, DELETE follow the same pattern
41
- export const PUT = defineRoute({ body: z.object({...}), handler: ({body, params}) => {...} })
42
- export const DELETE = defineRoute({ params: z.object({id: z.coerce.number()}), handler: ({params}) => {...} })
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}) => {...} })
43
45
  ```
44
46
 
47
+ ## policy — who may call this route
48
+
49
+ Required on every exported method. The scanner refuses a route file that omits it and names the
50
+ file, so absence is a build error rather than a route silently open to everyone.
51
+
52
+ ```typescript
53
+ policy: 'public' // open, and said out loud
54
+ policy: ({ subject }) => subject !== null // any authenticated caller
55
+ policy: ({ subject, params }) => requireOwner(subject, ownerOf(params.id)) // this subject owns this record
56
+ ```
57
+
58
+ `requireOwner` comes from `theokit/server`. The policy is evaluated identically over HTTP and
59
+ in-process, so a desktop or terminal surface gets the same answer a browser does. It receives no
60
+ headers and no cookies: identity arrives as `subject`, established by the transport.
61
+
45
62
  ## File-to-URL Mapping
46
63
 
47
- | File path | URL | Notes |
48
- |-----------|-----|-------|
49
- | `server/routes/health.ts` | `GET /api/health` | Static route |
50
- | `server/routes/tasks/index.ts` | `/api/tasks` | Index route (GET + POST) |
51
- | `server/routes/tasks/[id].ts` | `/api/tasks/:id` | Dynamic param |
52
- | `server/routes/users/[...slug].ts` | `/api/users/*` | Catch-all |
64
+ | File path | URL | Notes |
65
+ | ---------------------------------- | ----------------- | ------------------------ |
66
+ | `server/routes/health.ts` | `GET /api/health` | Static route |
67
+ | `server/routes/tasks/index.ts` | `/api/tasks` | Index route (GET + POST) |
68
+ | `server/routes/tasks/[id].ts` | `/api/tasks/:id` | Dynamic param |
69
+ | `server/routes/users/[...slug].ts` | `/api/users/*` | Catch-all |
53
70
 
54
71
  ## defineAction (Server Actions)
55
72
 
@@ -71,6 +88,7 @@ export const createTask = defineAction({
71
88
  import { TheoError } from 'theokit'
72
89
 
73
90
  export const GET = defineRoute({
91
+ policy: 'public',
74
92
  handler: ({ params }) => {
75
93
  const task = db.select().from(tasks).where(eq(tasks.id, params.id)).get()
76
94
  if (!task) throw new TheoError({ code: 'NOT_FOUND', message: 'Task not found' })
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: theokit-ui
3
- description: "@theokit/ui AI-native component library — AI-agent surfaces (ChatThread, ChatMessage, ChatComposer, ToolCallCard, AgentStream), theming, providers; generic primitives (CodeBlock, Sidebar, Button) come from @usetheo/ui"
3
+ description: '@theokit/ui AI-native component library — AI-agent surfaces (ChatThread, ChatMessage, ChatComposer, ToolCallCard, AgentStream), theming, providers; generic primitives (CodeBlock, Sidebar, Button) come from @usetheo/ui'
4
4
  user-invocable: false
5
5
  paths:
6
- - "app/**"
7
- - "**/*Chat*"
8
- - "**/*chat*"
9
- - "**/*Sidebar*"
10
- - "**/*sidebar*"
11
- - "**/*theme*"
12
- - "**/*Theme*"
6
+ - 'app/**'
7
+ - '**/*Chat*'
8
+ - '**/*chat*'
9
+ - '**/*Sidebar*'
10
+ - '**/*sidebar*'
11
+ - '**/*theme*'
12
+ - '**/*Theme*'
13
13
  ---
14
14
 
15
15
  # @theokit/ui — AI-native Component Library (AI-agent surfaces)
@@ -99,31 +99,32 @@ function ChatPage() {
99
99
 
100
100
  ### AI-agent-surface components (from `@theokit/ui`)
101
101
 
102
- | Component | Purpose | Key Props |
103
- |-----------|---------|-----------|
104
- | `ChatThread` | Scrollable message container | `children` (ChatMessage elements) |
105
- | `ChatMessage` | Single message bubble | `role: 'user' \| 'assistant'`, `children` |
106
- | `ChatMessageContent` | Markdown + code rendering | `markdown: string` (handles streaming partial) |
107
- | `ChatComposer` | Message input + submit | `onSubmit: (text) => void`, `disabled?: boolean` |
108
- | `ToolCallCard` | Display agent tool invocations | — |
109
- | `AgentStream` | Lower-level stream renderer | — |
102
+ | Component | Purpose | Key Props |
103
+ | -------------------- | ------------------------------ | ------------------------------------------------ |
104
+ | `ChatThread` | Scrollable message container | `children` (ChatMessage elements) |
105
+ | `ChatMessage` | Single message bubble | `role: 'user' \| 'assistant'`, `children` |
106
+ | `ChatMessageContent` | Markdown + code rendering | `markdown: string` (handles streaming partial) |
107
+ | `ChatComposer` | Message input + submit | `onSubmit: (text) => void`, `disabled?: boolean` |
108
+ | `ToolCallCard` | Display agent tool invocations | — |
109
+ | `AgentStream` | Lower-level stream renderer | — |
110
110
 
111
111
  ### Generic primitives (from `@usetheo/ui`)
112
112
 
113
113
  Moved out of `@theokit/ui` in the 2026-07-03 AI-exclusive pivot. Import these from `@usetheo/ui`.
114
114
 
115
- | Component | Purpose | Key Props |
116
- |-----------|---------|-----------|
117
- | `CodeBlock` | Syntax-highlighted code | `code: string`, `language?: string` (uses shiki, lazy-loaded) |
118
- | `PageShell` | App layout with sidebar slot | `sidebar?: ReactNode`, `children` |
119
- | `Sidebar` | Collapsible side panel | `children` |
120
- | `Button`, `Input`, `Textarea` | Form primitives (themed) | — |
121
- | `Avatar` | User/agent avatar | — |
122
- | `Alert` | Status messages | — |
115
+ | Component | Purpose | Key Props |
116
+ | ----------------------------- | ---------------------------- | ------------------------------------------------------------- |
117
+ | `CodeBlock` | Syntax-highlighted code | `code: string`, `language?: string` (uses shiki, lazy-loaded) |
118
+ | `PageShell` | App layout with sidebar slot | `sidebar?: ReactNode`, `children` |
119
+ | `Sidebar` | Collapsible side panel | `children` |
120
+ | `Button`, `Input`, `Textarea` | Form primitives (themed) | — |
121
+ | `Avatar` | User/agent avatar | — |
122
+ | `Alert` | Status messages | — |
123
123
 
124
124
  ## Peer Dependencies (install only what you use)
125
125
 
126
126
  **Chat/markdown path** (most apps need these):
127
+
127
128
  ```bash
128
129
  npm install mdast-util-from-markdown mdast-util-to-hast mdast-util-gfm \
129
130
  hast-util-to-jsx-runtime hast-util-sanitize hast-util-from-html \
@@ -131,6 +132,7 @@ npm install mdast-util-from-markdown mdast-util-to-hast mdast-util-gfm \
131
132
  ```
132
133
 
133
134
  **DO NOT install** unless you use the specific components:
135
+
134
136
  - `mermaid` — only for diagram rendering components
135
137
  - `katex` — only for math/LaTeX rendering
136
138
  - `roughjs` / `perfect-freehand` — only for whiteboard/drawing components
@@ -2,7 +2,14 @@ import tseslint from 'typescript-eslint'
2
2
  import prettierConfig from 'eslint-config-prettier'
3
3
 
4
4
  export default tseslint.config(
5
- { ignores: ['dist/', 'node_modules/'] },
5
+ // `.theokit/` is build output — `theokit build` writes generated `.d.ts` there, and linting it
6
+ // reports ~1800 style findings in code nobody wrote. A developer who adds a real error of their
7
+ // own then cannot find it, which is the whole cost: not a broken build, a useless gate.
8
+ //
9
+ // ESLint flat config does NOT read `.gitignore`, so the one file that already knows what is
10
+ // generated is not the file ESLint consults. `@eslint/compat`'s `includeIgnoreFile` would close
11
+ // that permanently and is worth considering; this is the one-line version that works today.
12
+ { ignores: ['dist/', 'node_modules/', '.theokit/'] },
6
13
  ...tseslint.configs.recommended,
7
14
  prettierConfig,
8
15
  {