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.
- package/dist/cli.js +55 -44
- package/dist/cli.js.map +1 -1
- package/package.json +2 -1
- package/templates/default/.env.example +1 -1
- package/templates/default/CLAUDE.md +9 -8
- package/templates/default/_gitignore +5 -2
- package/templates/default/_prettierignore +13 -0
- package/templates/default/agents/chat.ts +29 -1
- package/templates/default/app/components/ChatPanel.tsx +3 -2
- package/templates/default/app/hooks/use-transcript.ts +9 -3
- package/templates/default/app/layout.tsx +8 -1
- package/templates/default/app/lib/constants.ts +2 -1
- package/templates/default/app/lib/renderable.ts +78 -0
- package/templates/default/app/page.test.tsx +9 -3
- package/templates/default/docs/ARCHITECTURE.md +35 -29
- package/templates/default/docs/CUSTOMIZATION.md +10 -10
- package/templates/default/docs/ENVIRONMENT.md +4 -4
- package/templates/default/dot-claude/rules/theokit-conventions.md +2 -0
- package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +12 -12
- package/templates/default/dot-claude/skills/theokit-config/SKILL.md +6 -6
- package/templates/default/dot-claude/skills/theokit-database/SKILL.md +15 -10
- package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +11 -11
- package/templates/default/dot-claude/skills/theokit-gateways/SKILL.md +71 -0
- package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +28 -10
- package/templates/default/dot-claude/skills/theokit-ui/SKILL.md +26 -24
- package/templates/default/eslint.config.mjs +8 -1
- package/templates/default/package.json.tmpl +2 -8
- package/templates/default/pnpm-workspace.yaml +21 -0
- package/templates/default/server/routes/health.ts +3 -0
- 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
|
|
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('
|
|
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)
|
|
64
|
-
.tool(weatherTool)
|
|
65
|
-
.tool(currentTimeTool)
|
|
66
|
-
.skills([dailyBriefingSkill])
|
|
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
|
|
83
|
-
| tui
|
|
84
|
-
| desktop | `frontend/`
|
|
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
|
|
93
|
-
|
|
94
|
-
| `app/` root
|
|
95
|
-
| `components/` | presentational UI (flat `.tsx`; Tailwind, no CSS modules) | `Header`, `ChatPanel`, `Composer`
|
|
96
|
-
| `hooks/`
|
|
97
|
-
| `lib/`
|
|
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 —
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
133
|
-
|
|
134
|
-
| a `/settings` screen | `app/settings/page.tsx`
|
|
135
|
-
| a nested screen
|
|
136
|
-
| a dynamic screen
|
|
137
|
-
| a catch-all
|
|
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'
|
|
148
|
-
import { useLocation } from 'react-router'
|
|
149
|
-
|
|
150
|
-
|
|
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…
|
|
6
|
-
|
|
7
|
-
| Change the model
|
|
8
|
-
| Change the persona / rules
|
|
9
|
-
| Add a tool (an action the agent can take)
|
|
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
|
|
12
|
-
| Change the greeting / app name
|
|
13
|
-
| Add a backend route
|
|
14
|
-
| Change the provider key
|
|
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
|
|
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`
|
|
9
|
-
| `OPENAI_API_KEY`
|
|
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
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
|
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
|
|
166
|
-
|
|
167
|
-
| `@theokit/sdk`
|
|
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`
|
|
170
|
-
| `@theokit/di`
|
|
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
|
-
-
|
|
7
|
-
-
|
|
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',
|
|
20
|
-
port: 3000,
|
|
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,
|
|
28
|
-
csp: 'report-only',
|
|
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
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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')
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
-
|
|
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
|
|
14
|
-
|
|
15
|
-
| `app/page.tsx`
|
|
16
|
-
| `app/layout.tsx`
|
|
17
|
-
| `app/error.tsx`
|
|
18
|
-
| `app/loading.tsx`
|
|
19
|
-
| `app/not-found.tsx`
|
|
20
|
-
| `app/about/page.tsx`
|
|
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'
|
|
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
|
-
-
|
|
7
|
-
-
|
|
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
|
|
48
|
-
|
|
49
|
-
| `server/routes/health.ts`
|
|
50
|
-
| `server/routes/tasks/index.ts`
|
|
51
|
-
| `server/routes/tasks/[id].ts`
|
|
52
|
-
| `server/routes/users/[...slug].ts` | `/api/users/*`
|
|
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:
|
|
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
|
-
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
|
103
|
-
|
|
104
|
-
| `ChatThread`
|
|
105
|
-
| `ChatMessage`
|
|
106
|
-
| `ChatMessageContent` | Markdown + code rendering
|
|
107
|
-
| `ChatComposer`
|
|
108
|
-
| `ToolCallCard`
|
|
109
|
-
| `AgentStream`
|
|
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
|
|
116
|
-
|
|
117
|
-
| `CodeBlock`
|
|
118
|
-
| `PageShell`
|
|
119
|
-
| `Sidebar`
|
|
120
|
-
| `Button`, `Input`, `Textarea` | Form primitives (themed)
|
|
121
|
-
| `Avatar`
|
|
122
|
-
| `Alert`
|
|
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
|
-
|
|
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
|
{
|