create-theokit 1.25.3 → 2.0.0-next.1

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 (55) hide show
  1. package/dist/cli.js +53 -54
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +8 -1
  4. package/templates/_bot-preset/{agents → src/server/agents}/tools/publish.ts +1 -1
  5. package/templates/_bot-preset/{server → src/server}/crons/daily-research.ts +2 -2
  6. package/templates/default/AGENTS.md.tmpl +58 -0
  7. package/templates/default/CLAUDE.md +4 -4
  8. package/templates/default/README.md.tmpl +56 -30
  9. package/templates/default/docs/ARCHITECTURE.md +103 -58
  10. package/templates/default/docs/CUSTOMIZATION.md +10 -10
  11. package/templates/default/docs/ENVIRONMENT.md +6 -6
  12. package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +1 -1
  13. package/templates/default/dot-claude/skills/theokit-gateways/SKILL.md +1 -1
  14. package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +1 -1
  15. package/templates/default/dot-claude/skills/theokit-ui/SKILL.md +1 -1
  16. package/templates/default/dot-theokit/THEO.md +54 -0
  17. package/templates/default/dot-theokit/personalities/README.md +35 -0
  18. package/templates/default/dot-theokit/personalities/concise.md +14 -0
  19. package/templates/default/dot-theokit/personalities/teacher.md +17 -0
  20. package/templates/default/dot-theokit/rules/README.md +30 -0
  21. package/templates/default/dot-theokit/rules/server-routes.md +16 -0
  22. package/templates/default/package.json.tmpl +2 -2
  23. package/templates/default/{agents → src/server/agents}/chat.ts +6 -0
  24. package/templates/default/src/server/agents/hooks/tool-audit.ts +75 -0
  25. package/templates/default/theo.config.ts +18 -1
  26. package/templates/default/tsconfig.json +4 -5
  27. /package/templates/_bot-preset/{agents → src/server/agents}/lib/bot-scope.ts +0 -0
  28. /package/templates/_bot-preset/{agents → src/server/agents}/lib/sandbox.ts +0 -0
  29. /package/templates/_bot-preset/{agents → src/server/agents}/publisher.ts +0 -0
  30. /package/templates/_bot-preset/{agents → src/server/agents}/researcher.ts +0 -0
  31. /package/templates/_bot-preset/{agents → src/server/agents}/tools/read-notes.ts +0 -0
  32. /package/templates/_bot-preset/{agents → src/server/agents}/tools/write-note.ts +0 -0
  33. /package/templates/_bot-preset/{server → src/server}/delivery.ts +0 -0
  34. /package/templates/default/{app → src/app}/about/page.tsx +0 -0
  35. /package/templates/default/{app → src/app}/components/ChatPanel.tsx +0 -0
  36. /package/templates/default/{app → src/app}/components/Composer.tsx +0 -0
  37. /package/templates/default/{app → src/app}/components/Header.tsx +0 -0
  38. /package/templates/default/{app → src/app}/components/Nav.tsx +0 -0
  39. /package/templates/default/{app → src/app}/error.tsx +0 -0
  40. /package/templates/default/{app → src/app}/hooks/use-transcript.test.ts +0 -0
  41. /package/templates/default/{app → src/app}/hooks/use-transcript.ts +0 -0
  42. /package/templates/default/{app → src/app}/layout.tsx +0 -0
  43. /package/templates/default/{app → src/app}/lib/constants.ts +0 -0
  44. /package/templates/default/{app → src/app}/lib/renderable.ts +0 -0
  45. /package/templates/default/{app → src/app}/loading.tsx +0 -0
  46. /package/templates/default/{app → src/app}/not-found.tsx +0 -0
  47. /package/templates/default/{app → src/app}/page.test.tsx +0 -0
  48. /package/templates/default/{app → src/app}/page.tsx +0 -0
  49. /package/templates/default/{agents → src/server/agents}/prompts/instructions.ts +0 -0
  50. /package/templates/default/{agents → src/server/agents}/skills/daily-briefing.ts +0 -0
  51. /package/templates/default/{agents → src/server/agents}/tools/current-time.ts +0 -0
  52. /package/templates/default/{agents → src/server/agents}/tools/send-notification.ts +0 -0
  53. /package/templates/default/{agents → src/server/agents}/tools/weather.ts +0 -0
  54. /package/templates/default/{server → src/server}/routes/health.ts +0 -0
  55. /package/templates/default/{shared → src/shared}/agent.ts +0 -0
@@ -1,60 +1,105 @@
1
1
  # Architecture
2
2
 
3
- How this TheoKit app is organized. The layout puts the **agent at the center**: the agent file and the
4
- folders it composes (prompts, tools, skills) live together under `agents/`, with clean namesthe
3
+ How this TheoKit app is organized. Two domains, named by what they are rather than by what they
4
+ contain: `src/app/` is the interface the only code that reaches a browserand `src/server/` is
5
+ the backend, everything that never leaves the machine.
6
+
7
+ The **agent lives inside the backend**, at `src/server/agents/`, because that is what it is: it
8
+ reads your API keys, calls tools that touch the network, and runs where the server runs. Every rule
9
+ that applies to `server/` applies to it. Within that folder the agent is still at the center — the
10
+ agent file and what composes it (prompts, tools, skills) sit together with clean names, the
5
11
  "file = identity" convention agent frameworks like [Eve](https://eve.dev) use.
6
12
 
7
13
  ## Project structure
8
14
 
9
15
  ```
10
16
  .
11
- ├── agents/ # The agent, and what composes it
12
- │ ├── chat.ts # the agent POST /api/agents/chat, useAgent('chat')
13
- │ ├── prompts/ # system prompts / personas
14
- │ │ └── instructions.ts
15
- │ ├── tools/ # tools the agent can call
16
- │ │ ├── weather.ts # remote — current weather via open-meteo (HTTP)
17
- │ │ └── current-time.ts # local date/time in an IANA timezone (no network)
18
- └── skills/ # procedures the model loads on demand (Skill.create)
19
- └── daily-briefing.ts # a real skill: time weather a one-line nudge
20
- ├── app/ # Frontend (web surface). tui tui/, desktop → frontend/
21
- ├── page.tsx # the `/` route composition root (lays out the components + the hook)
22
- ├── layout.tsx # root layout — composes <Header/> over the routed page
23
- ├── error/loading/not-found.tsx # route surface (special files)
24
- ├── about/page.tsx # an EXAMPLE `/about` route — shows how screens grow (delete when done)
25
- │ ├── components/ # presentational UI (Tailwind, flat .tsx no CSS modules)
26
- │ │ ├── Header.tsx # the top bar (composes Nav + theme toggle)
27
- │ │ ├── Nav.tsx # the navigation menu (a link per screen)
28
- │ │ ├── ChatPanel.tsx # the transcript + streaming indicator + starter prompts
29
- │ │ └── Composer.tsx # the input + error card + new-chat
30
- │ ├── hooks/ # custom hooks
31
- │ │ └── use-transcript.ts # transcript STATE (history + streaming)
32
- └── lib/ # app modules / config
33
- └── constants.ts # greeting + starter prompts
34
- ├── server/ # Backend routes / actions (POST/GET handlers, jobs)
35
- ├── shared/ # Code imported by more than one layer
36
- │ └── agent.ts # branding (name, model, greeting) — one source of truth
17
+ ├── src/
18
+ │ ├── server/ # Backend everything that never leaves the machine
19
+ ├── agents/ # The agent, and what composes it
20
+ │ │ │ ├── chat.ts # the agent → POST /api/agents/chat, useAgent('chat')
21
+ │ │ ├── prompts/ # system prompts / personas
22
+ │ │ │ │ └── instructions.ts
23
+ │ │ │ ├── tools/ # tools the agent can call
24
+ │ │ │ ├── weather.ts # remote current weather via open-meteo (HTTP)
25
+ │ │ │ └── current-time.ts # local date/time in an IANA timezone (no network)
26
+ │ │ │ ├── skills/ # procedures the model loads on demand (Skill.create)
27
+ │ │ │ └── daily-briefing.ts # a real skill: time weather a one-line nudge
28
+ │ │ └── hooks/ # lifecycle interception
29
+ │ │ └── tool-audit.ts # one structured line per tool call, with timing
30
+ │ └── routes/ # Backend routes / actions (POST/GET handlers, jobs)
31
+ │ ├── app/ # Frontend (web surface). tui tui/, desktop → frontend/
32
+ │ │ ├── page.tsx # the `/` route — composition root (components + the hook)
33
+ │ │ ├── layout.tsx # root layout composes <Header/> over the routed page
34
+ │ │ ├── error/loading/not-found.tsx # route surface (special files)
35
+ │ │ ├── about/page.tsx # an EXAMPLE `/about` route (delete when done)
36
+ ├── components/ # presentational UI (Tailwind, flat .tsx — no CSS modules)
37
+ │ │ │ ├── Header.tsx # the top bar (composes Nav + theme toggle)
38
+ │ │ ├── Nav.tsx # the navigation menu (a link per screen)
39
+ │ │ ├── ChatPanel.tsx # the transcript + streaming indicator + starters
40
+ │ │ │ └── Composer.tsx # the input + error card + new-chat
41
+ │ │ ├── hooks/ # custom hooks
42
+ │ │ └── use-transcript.ts # transcript STATE (history + streaming)
43
+ │ │ └── lib/ # app modules / config
44
+ │ │ └── constants.ts # greeting + starter prompts
45
+ │ └── shared/ # Code imported by BOTH sides — and only that
46
+ │ └── agent.ts # branding (name, model, greeting) — one source of truth
47
+ ├── .theokit/ # Agent context, read at runtime — no rebuild to change it
48
+ │ ├── THEO.md # prepended every turn (priority 60)
49
+ │ ├── rules/ # path-scoped instructions, by glob (priority 45)
50
+ │ └── personalities/ # swappable system prompts (agent.usePersonality)
37
51
  ├── types/ # Framework ambient declarations (e.g. the job registry)
38
52
  ├── docs/ # This folder
39
53
  └── theo.config.ts # App config (name, dirs, plugins)
40
54
  ```
41
55
 
56
+ ## Two halves: what you compile, and what the agent reads
57
+
58
+ `src/server/agents/` is **code** — it changes when you deploy. `.theokit/` is **data** — the agent
59
+ reads it at runtime, so a wording change is an edit, not a release.
60
+
61
+ The SDK loads context from seven sources in priority order, and yours sit inside it:
62
+
63
+ | Priority | Source | Scope |
64
+ | -------- | ------------------------- | ------------------------ |
65
+ | 10 | `AGENTS.md` | walks up to the git root |
66
+ | 20 | `GEMINI.md` | walks up to the git root |
67
+ | 30 | `CLAUDE.md` | walks up to the git root |
68
+ | 40 | `.cursor/rules/*.mdc` | glob-activated |
69
+ | **45** | **`.theokit/rules/*.md`** | **glob-activated** |
70
+ | 50 | `.theokit/context/*.md` | glob-activated |
71
+ | **60** | **`.theokit/THEO.md`** | this directory only |
72
+
73
+ Later wins where they conflict, so `THEO.md` is the strongest thing in the file layer — which is
74
+ why it should hold facts rather than preferences. A preference that always wins is a preference you
75
+ cannot override with a personality.
76
+
77
+ **Personalities are not in that table**, and the reason is worth keeping straight: the seven sources
78
+ above are _context files_, merged into every relevant turn. A personality REPLACES the system
79
+ prompt, only while it is active, and only when someone asks for it:
80
+
81
+ ```ts
82
+ await agent.usePersonality('teacher', { save: true })
83
+ ```
84
+
85
+ Reserved names — `none`, `default`, `neutral` — clear it.
86
+
42
87
  ## Clean names, no phantom routes
43
88
 
44
- An agent is a file: `agents/<name>.ts` → `POST /api/agents/<name>`. But the framework's scanner is
45
- **folder-semantic** — thirteen sub-folder names under `agents/` (`tools/`, `skills/`, `prompts/`,
89
+ An agent is a file: `src/server/agents/<name>.ts` → `POST /api/agents/<name>`. But the framework's scanner is
90
+ **folder-semantic** — thirteen sub-folder names under `src/server/agents/` (`tools/`, `skills/`, `prompts/`,
46
91
  `lib/`, `hooks/`, `channels/`, `connections/`, `subagents/`, `schedules/`, `sandbox/`, `workflows/`,
47
92
  `evals/`, `memory/`) are **that concern, not routes**.
48
- So the names stay clean (`tools/`, not `_tools/`) and `agents/tools/weather.ts` never becomes a phantom
93
+ So the names stay clean (`tools/`, not `_tools/`) and `src/server/agents/tools/weather.ts` never becomes a phantom
49
94
  `/api/agents/tools/weather` endpoint. Markdown (`skills/*.md`) is never scanned either way. The
50
- prompts/tools/skills are **shared** across every agent in `agents/`.
95
+ prompts/tools/skills are **shared** across every agent in `src/server/agents/`.
51
96
 
52
97
  An agent that outgrows one file becomes a folder that co-locates its own composition —
53
- `agents/<name>/index.ts` with `tools/` and `prompts/` beside it. Same route, same identity.
98
+ `src/server/agents/<name>/index.ts` with `tools/` and `prompts/` beside it. Same route, same identity.
54
99
 
55
100
  ## Composition
56
101
 
57
- `agents/chat.ts` is thin on purpose — it wires the pieces together:
102
+ `src/server/agents/chat.ts` is thin on purpose — it wires the pieces together:
58
103
 
59
104
  ```ts
60
105
  export default AgentBuilder.create()
@@ -71,7 +116,7 @@ Grow the agent by editing its neighbours, not by inflating `chat.ts`: persona
71
116
  a new capability → `tools/<name>.ts` (then `.tool(<name>Tool)`), a documented procedure → a
72
117
  `Skill.create(...)` in `skills/<name>.ts` (add it to the `.skills([...])` list). A **skill** is
73
118
  loaded by the model on demand via the `skill_read` tool, so long procedures don't bloat every prompt. Add a
74
- **second agent** as another `agents/<name>.ts`.
119
+ **second agent** as another `src/server/agents/<name>.ts`.
75
120
 
76
121
  ## Surfaces
77
122
 
@@ -79,34 +124,34 @@ The same agent is reached from three interchangeable frontends — only the tran
79
124
 
80
125
  | Surface | Frontend dir | Transport |
81
126
  | ------- | ------------ | ------------------------------------- |
82
- | web | `app/` | `HttpTransport` (`/api/agents/chat`) |
127
+ | web | `src/app/` | `HttpTransport` (`/api/agents/chat`) |
83
128
  | tui | `tui/` | `InProcessTransport` |
84
129
  | desktop | `frontend/` | `ChannelTransport` (`@theokit/tauri`) |
85
130
 
86
- All three render the same `@theokit/ui` chat and read `shared/agent.ts` for the greeting + model label.
131
+ All three render the same `@theokit/ui` chat and read `src/shared/agent.ts` for the greeting + model label.
87
132
 
88
133
  ## Frontend organization
89
134
 
90
- The web `app/` is organized **type-based**, the layout most React apps grow into:
135
+ The web `src/app/` is organized **type-based**, the layout most React apps grow into:
91
136
 
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` |
137
+ | Folder | Holds | Example |
138
+ | --------------- | --------------------------------------------------------- | ------------------------------------------------------- |
139
+ | `src/app/` root | the **route surface** — the only files the router serves | `page.tsx`, `layout.tsx`, `error/loading/not-found.tsx` |
140
+ | `components/` | presentational UI (flat `.tsx`; Tailwind, no CSS modules) | `Header`, `ChatPanel`, `Composer` |
141
+ | `hooks/` | custom hooks — where **state** lives | `use-transcript.ts` |
142
+ | `lib/` | app modules / config | `constants.ts` |
98
143
 
99
144
  ### Route files are not components
100
145
 
101
146
  `page` · `layout` · `loading` · `error` · `not-found` are **route conventions**, not components. The
102
147
  router binds them by **name + location**: matched by `^(page|layout|error|loading|not-found)\.(tsx|ts|jsx|js)$`
103
- at a route segment (the `app/` root is the `/` route). `loading.tsx` becomes that route's Suspense
148
+ at a route segment (the `src/app/` root is the `/` route). `loading.tsx` becomes that route's Suspense
104
149
  fallback, `error.tsx` its error boundary — _because they sit there, with that name_.
105
150
 
106
151
  So `loading.tsx` looks like a component (it renders a spinner) but it is **not** one — move it into
107
152
  `components/` and the router no longer finds it, and the route loses its loading UI. This is exactly the
108
153
  Next.js App Router model TheoKit implements: the special files live at the route segment, and `components/`
109
- / `hooks/` / `lib/` sit alongside them. Having both at the `app/` root is the convention, not a mismatch.
154
+ / `hooks/` / `lib/` sit alongside them. Having both at the `src/app/` root is the convention, not a mismatch.
110
155
  (If a special file grows big, keep the thin route file and have it render a real component from
111
156
  `components/` — e.g. `loading.tsx` → `<LoadingScreen/>`.)
112
157
 
@@ -126,17 +171,17 @@ a `pages/` folder you wire by hand: that's the Next.js-style convention TheoKit
126
171
 
127
172
  ### Adding a screen
128
173
 
129
- Routing is **file-based**: a screen is a folder under `app/` with a `page.tsx`. The folder name is the URL
130
- segment; the home screen is the flat `app/page.tsx`.
174
+ Routing is **file-based**: a screen is a folder under `src/app/` with a `page.tsx`. The folder name is the URL
175
+ segment; the home screen is the flat `src/app/page.tsx`.
131
176
 
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/*` |
177
+ | You want | Create | Serves |
178
+ | -------------------- | ----------------------------------- | ------------------------------------------------------------- |
179
+ | a `/settings` screen | `src/app/settings/page.tsx` | `/settings` |
180
+ | a nested screen | `src/app/settings/billing/page.tsx` | `/settings/billing` |
181
+ | a dynamic screen | `src/app/users/[id]/page.tsx` | `/users/:id` (read the param with react-router's `useParams`) |
182
+ | a catch-all | `src/app/docs/[...slug]/page.tsx` | `/docs/*` |
138
183
 
139
- The one-command way: **`theokit generate page settings`** creates `app/settings/page.tsx` for you. Each
184
+ The one-command way: **`theokit generate page settings`** creates `src/app/settings/page.tsx` for you. Each
140
185
  screen can have its own `layout.tsx` / `loading.tsx` / `error.tsx` / `not-found.tsx` (the route special
141
186
  files, scoped to that segment).
142
187
 
@@ -158,11 +203,11 @@ export function SettingsLink() {
158
203
 
159
204
  TheoKit's `theokit/client` gives you the Next-parity building blocks: **`Link`** (prefetch), **`Metadata`**
160
205
  (set `<title>`/meta per route — used in `page.tsx` + `about/page.tsx`), **`Image`** (optimized `<img>`),
161
- **`theoFetch`** + the **`theokit/react-query`** adapter (typed data fetching against your `server/`
206
+ **`theoFetch`** + the **`theokit/react-query`** adapter (typed data fetching against your `src/server/`
162
207
  routes), and **`useAgent`** (the chat stream). Reach for these instead of generic libraries.
163
208
 
164
- The primary menu is `app/components/Nav.tsx` (TheoKit `Link` + `useLocation` for the active route). A worked
165
- example ships as `app/about/page.tsx` (the `/about` route, linked from the `Nav`, with its own `Metadata`
209
+ The primary menu is `src/app/components/Nav.tsx` (TheoKit `Link` + `useLocation` for the active route). A worked
210
+ example ships as `src/app/about/page.tsx` (the `/about` route, linked from the `Nav`, with its own `Metadata`
166
211
  title); it explains this and tells you to delete it once you've got the idea.
167
212
 
168
213
  See also: [CUSTOMIZATION](./CUSTOMIZATION.md).
@@ -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 | `src/server/agents/chat.ts` (`.model(...)`) + the label in `src/shared/agent.ts` |
8
+ | Change the persona / rules | `src/server/agents/prompts/instructions.ts` |
9
+ | Add a tool (an action the agent can take) | new `src/server/agents/tools/<name>.ts`, then `.tool(<name>Tool)` in `src/server/agents/chat.ts` |
10
+ | Add a skill (a procedure the model loads on demand) | `Skill.create(...)` in `src/server/agents/skills/<name>.ts`, then add it to `.skills([...])` in `src/server/agents/chat.ts` |
11
+ | Add a second agent | new `src/server/agents/<name>.ts` → auto-served at `/api/agents/<name>`, bind with `useAgent('<name>')` |
12
+ | Change the greeting / app name | `src/shared/agent.ts` |
13
+ | Add a backend route | `src/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
 
@@ -8,17 +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 FIRST segment of the model id in `agents/chat.ts` picks the provider, and that decides which key
11
+ The FIRST segment of the model id in `src/server/agents/chat.ts` picks the provider, and that decides which key
12
12
  is needed. It is not a hint: `openai/gpt-4o-mini` goes to OpenAI and needs `OPENAI_API_KEY`, even
13
13
  with an OpenRouter key present. Reaching another vendor's catalog THROUGH OpenRouter means naming
14
14
  the gateway first — `openrouter/openai/gpt-4o-mini`, which is what the scaffold declares, matching
15
15
  the key above. See <https://openrouter.ai/models> for the ids OpenRouter serves.
16
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` |
17
+ | Model id in `src/server/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` |
22
22
 
23
23
  ```bash
24
24
  cp .env.example .env
@@ -3,7 +3,7 @@ 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
+ - 'src/app/**'
7
7
  ---
8
8
 
9
9
  # TheoKit Frontend (React + File-Based Routing)
@@ -3,7 +3,7 @@ name: theokit-gateways
3
3
  description: Receiving messages from Telegram, WhatsApp, Slack and other platforms — handleChannelWebhook, the @theokit/gateway-* adapters, signature validation, the onMessage seam
4
4
  user-invocable: false
5
5
  paths:
6
- - 'server/routes/**'
6
+ - 'src/server/routes/**'
7
7
  - 'server/channels/**'
8
8
  - '**/*gateway*'
9
9
  - '**/*webhook*'
@@ -3,7 +3,7 @@ name: theokit-routes
3
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
- - 'server/routes/**'
6
+ - 'src/server/routes/**'
7
7
  - 'server/actions/**'
8
8
  ---
9
9
 
@@ -3,7 +3,7 @@ name: theokit-ui
3
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/**'
6
+ - 'src/app/**'
7
7
  - '**/*Chat*'
8
8
  - '**/*chat*'
9
9
  - '**/*Sidebar*'
@@ -0,0 +1,54 @@
1
+ # Product context
2
+
3
+ Prepended to your agent's context on **every turn**, and it is the strongest source in the file
4
+ layer (priority 60) — nothing else overrides it. That is what decides what belongs here.
5
+
6
+ > **Not the same file as `AGENTS.md` at the root.** That one tells agents how to work on this
7
+ > codebase: commands, layout, conventions. This one tells your agent what your product IS, for the
8
+ > conversations it has with your users. If a sentence would help someone edit the code, it goes
9
+ > there instead.
10
+
11
+ **Put facts here, not preferences.** A preference written here wins against every personality, so a
12
+ tone instruction in this file quietly makes `usePersonality` do nothing. Tone belongs in
13
+ `.theokit/personalities/`; instructions about specific files belong in `.theokit/rules/`.
14
+
15
+ ## Why this file is here and not at the root
16
+
17
+ On the SDK this project installs (`^4.52.1`, which resolves to a 4.x), `.theokit/THEO.md` is the
18
+ **only** path a `THEO.md` is read from — a copy at the project root is read by nothing, silently.
19
+
20
+ `@theokit/sdk@5` adds a root `THEO.md` (`usetheokit/theokit-sdk#531`) at priority 55, so on that
21
+ version both locations work and `.theokit/THEO.md` still wins a conflict. 5.x is currently published
22
+ on the `next` channel only, so moving this file to the root would break it for anyone on the default
23
+ install — which is why the scaffold keeps it here.
24
+
25
+ Two more things worth knowing if you do move it once 5.x is stable:
26
+
27
+ - The root spec sets `followImports: true`, this one does not. `@file` references resolve there and
28
+ not here.
29
+ - Keep the pair. `AGENTS.md` is read by Cursor, Copilot and Claude Code as well as by TheoKit, and
30
+ it addresses a different audience — agents that write your code, rather than the agent that talks
31
+ to your users. That distinction survives whichever location this file ends up in.
32
+
33
+ Keep it short — every line costs tokens on every turn.
34
+
35
+ ## What this app is
36
+
37
+ Replace this with two or three sentences: what your product does, who uses it, and the one thing an
38
+ agent must never get wrong about it.
39
+
40
+ ## Vocabulary
41
+
42
+ The highest-value section, and the one most projects skip. Define the words your domain uses
43
+ differently from everyone else — a model that guesses what "account" means in your system will
44
+ guess wrong in a way that reads as fluent, which is the hardest kind of wrong to notice.
45
+
46
+ | Term | In this app it means |
47
+ | ------ | ---------------------------------------- |
48
+ | _term_ | _the definition your team actually uses_ |
49
+
50
+ ## Boundaries
51
+
52
+ What the agent should refuse or hand off, stated as fact rather than as tone. "Refunds are issued
53
+ by support, never by the assistant" is a fact. "Be careful with refunds" is a preference, and it
54
+ belongs in a personality where a user could switch it.
@@ -0,0 +1,35 @@
1
+ # Personalities
2
+
3
+ A personality is a **swappable system prompt**, chosen at runtime without redeploying:
4
+
5
+ ```ts
6
+ await agent.usePersonality('teacher') // for this process
7
+ await agent.usePersonality('teacher', { save: true }) // persists across restarts
8
+ await agent.usePersonality('none') // back to the agent's own system prompt
9
+ ```
10
+
11
+ `none`, `default` and `neutral` are reserved names that clear the active preset. History is kept
12
+ across a switch — pass `{ reset: true }` to clear the session too.
13
+
14
+ Each file is markdown: frontmatter describes it, the **body is the system prompt**.
15
+
16
+ | Frontmatter | Type | Meaning |
17
+ | ------------- | -------- | ---------------------------------------------- |
18
+ | `name` | string | the name you pass to `usePersonality` |
19
+ | `description` | string | shown when listing presets |
20
+ | `tools` | string[] | restrict the agent to these tools while active |
21
+ | `model` | string | override the model while active |
22
+ | `tags` | string[] | free-form grouping |
23
+
24
+ ## Personality, rule, or system prompt?
25
+
26
+ The three overlap enough that picking wrong is easy, and the difference is _when each one applies_:
27
+
28
+ | | Applies when | Changes at runtime |
29
+ | ------------------------ | -------------------------------------------- | ------------------------- |
30
+ | `chat.ts` `.system(...)` | always — it is what the agent IS | no, it ships in the build |
31
+ | a **personality** | while the user has it active | **yes**, `usePersonality` |
32
+ | a **rule** | when the conversation touches matching paths | no, but it is just a file |
33
+
34
+ Tone and stance belong in a personality. Facts about your domain belong in `THEO.md`. Instructions
35
+ about specific code belong in `rules/`.
@@ -0,0 +1,14 @@
1
+ ---
2
+ name: concise
3
+ description: Short answers, no preamble — for users who already know the domain
4
+ tags:
5
+ - default
6
+ ---
7
+
8
+ Answer in as few words as the question honestly allows.
9
+
10
+ - No preamble. Do not restate the question, do not announce what you are about to do.
11
+ - One paragraph unless the answer genuinely has parts. Then use a list.
12
+ - Skip the caveats a domain expert already knows. Keep the ones that would change their decision.
13
+ - When you are unsure, say so in one sentence and say what would resolve it. Do not pad uncertainty
14
+ into a long hedge — a long hedge reads as confidence.
@@ -0,0 +1,17 @@
1
+ ---
2
+ name: teacher
3
+ description: Explains the reasoning, for someone learning the domain
4
+ tags:
5
+ - onboarding
6
+ ---
7
+
8
+ Explain your reasoning, not just your conclusion.
9
+
10
+ - Lead with the answer, then say why it is the answer. Someone who already knew can stop at the
11
+ first line; someone learning reads on.
12
+ - Name the thing you are doing when it has a name. A reader who learns the word can look it up
13
+ later; a reader who only sees the steps cannot.
14
+ - When there was a real alternative, say what it was and why you did not take it. The rejected
15
+ option is usually the more instructive half.
16
+ - Never simplify to the point of being wrong. If the honest answer is complicated, say it is
17
+ complicated and then give it.
@@ -0,0 +1,30 @@
1
+ # Rules
2
+
3
+ Markdown files here are **path-scoped instructions**: they enter the agent's context only when the
4
+ conversation touches a file matching their `globs`. That is what separates a rule from `THEO.md`,
5
+ which is always present.
6
+
7
+ ```markdown
8
+ ---
9
+ description: what this rule is for
10
+ globs:
11
+ - src/server/routes/**/*.ts
12
+ ---
13
+
14
+ Your instructions here.
15
+ ```
16
+
17
+ | Frontmatter | Type | Meaning |
18
+ | ------------- | -------- | ---------------------------------- |
19
+ | `description` | string | what the rule covers |
20
+ | `globs` | string[] | paths that activate it |
21
+ | `paths` | string[] | same idea, explicit paths |
22
+ | `alwaysApply` | boolean | ignore the globs and always attach |
23
+ | `enabled` | boolean | switch it off without deleting it |
24
+
25
+ A file with **no frontmatter at all** is treated as `alwaysApply: true`. That is a convenience worth
26
+ knowing about and easy to trigger by accident: a rule you meant to scope, whose frontmatter has a
27
+ typo, becomes a rule that fires on every turn.
28
+
29
+ Rules load at priority 45 — after `CLAUDE.md` (30) and `.cursor/rules` (40), before
30
+ `.theokit/context` (50) and `THEO.md` (60).
@@ -0,0 +1,16 @@
1
+ ---
2
+ description: Conventions for backend route handlers
3
+ globs:
4
+ - src/server/routes/**/*.ts
5
+ ---
6
+
7
+ # Route handler conventions
8
+
9
+ These apply when the conversation touches a file under `src/server/routes/`.
10
+
11
+ - A route validates its input at the boundary with Zod and trusts it afterwards. A handler that
12
+ re-checks the same shape three layers down is describing distrust of its own validation.
13
+ - Return a typed error, never a bare `null` or `-1`. The caller cannot tell a missing value from a
14
+ failure, and one of those is a bug worth an alert.
15
+ - Read secrets from the environment inside the handler, never at module scope — a module-level read
16
+ runs at import time, which is before the process has decided whether it is a build or a boot.
@@ -16,8 +16,8 @@
16
16
  "typecheck": "tsc --noEmit"
17
17
  },
18
18
  "dependencies": {
19
- "theokit": "^0.60.0",
20
- "@theokit/agents": "^10.1.0",
19
+ "theokit": "^0.64.0",
20
+ "@theokit/agents": "^12.1.0",
21
21
  "@theokit/sdk": "^4.52.1",
22
22
  "@theokit/ui": "^1.1.0",
23
23
  "@usetheo/ui": "^0.26.0",
@@ -2,6 +2,7 @@ import { AgentBuilder } from '@theokit/agents'
2
2
  import { z } from 'zod'
3
3
 
4
4
  import { BASE_INSTRUCTIONS } from './prompts/instructions.js'
5
+ import { toolAuditHooks } from './hooks/tool-audit.js'
5
6
  import { dailyBriefingSkill } from './skills/daily-briefing.js'
6
7
  import { currentTimeTool } from './tools/current-time.js'
7
8
  import { sendNotificationTool } from './tools/send-notification.js'
@@ -67,4 +68,9 @@ export default AgentBuilder.create()
67
68
  .approval('send_notification', { question: 'Send this notification?' })
68
69
  // Skills the agent can consult on demand (hover `.skills` for how it works).
69
70
  .skills([dailyBriefingSkill])
71
+ // Lifecycle hooks — one structured log line per tool call, with its duration. This is the
72
+ // observability half of the wiring: without it, a slow or looping agent is something you infer
73
+ // from a complaint instead of something you read in a log. `hooks/tool-audit.ts` explains the
74
+ // other seven events, and why this one deliberately does not veto anything.
75
+ .hooks(toolAuditHooks)
70
76
  .build()
@@ -0,0 +1,75 @@
1
+ import type { HookHandlers } from '@theokit/agents'
2
+
3
+ /**
4
+ * Lifecycle hooks — the seam where you observe or intercept the agent loop.
5
+ *
6
+ * A tool call is the moment your agent stops talking and starts *doing*: it hits the network, writes
7
+ * a row, sends a message. This hook makes every one of those visible, with the timing, so a slow or
8
+ * looping agent is something you can see in a log rather than something you infer from a user
9
+ * complaint.
10
+ *
11
+ * Eight events exist. These two are the pair you almost always want first:
12
+ *
13
+ * | Event | When | Can it stop the call? |
14
+ * |---|---|---|
15
+ * | `pre_tool_call` | before the tool runs | **yes** — return `{ block: true, message }` |
16
+ * | `post_tool_call` | after it returns | no — fire-and-forget |
17
+ * | `transform_tool_result` | folds the turn's results before the model sees them | rewrites |
18
+ * | `transform_llm_output` | folds the model's text | rewrites |
19
+ * | `on_session_start` / `on_session_end` | session lifecycle | no |
20
+ * | `pre_user_send` / `post_assistant_reply` | around a user turn | no |
21
+ *
22
+ * ## Why this one only observes
23
+ *
24
+ * `pre_tool_call` is the only hook with veto power, and it is tempting to ship a template that uses
25
+ * it. It is deliberately not used here: a veto encodes a policy, and any policy this file could
26
+ * invent would be one your app did not ask for. `send_notification` is already gated the right way
27
+ * — by a human approval in `chat.ts` — which is where a decision belongs when a person should make
28
+ * it. Reach for the veto when you have a rule of your own:
29
+ *
30
+ * ```ts
31
+ * pre_tool_call: (ctx) =>
32
+ * ctx.name === 'delete_account' && !isAdmin(ctx)
33
+ * ? { block: true, message: 'Only an admin may delete an account.' }
34
+ * : undefined,
35
+ * ```
36
+ *
37
+ * The message goes to the MODEL, not the user — write it as an instruction the agent can act on.
38
+ */
39
+
40
+ /** Start times by `${runId}:${name}`, so a duration survives concurrent calls in one run. */
41
+ const started = new Map<string, number>()
42
+
43
+ function key(runId: string, name: string): string {
44
+ return `${runId}:${name}`
45
+ }
46
+
47
+ export const toolAuditHooks: HookHandlers = {
48
+ pre_tool_call: (ctx) => {
49
+ started.set(key(ctx.runId, ctx.name), Date.now())
50
+ // One structured line per call. Structured, not a sentence, because the thing you will want
51
+ // later is a filter (`tool=weather`), and a sentence has to be parsed back apart to give you one.
52
+ console.info(
53
+ JSON.stringify({ event: 'tool.start', tool: ctx.name, runId: ctx.runId, args: ctx.args }),
54
+ )
55
+ // `undefined` lets the call through. Returning nothing is not the same as approving nothing:
56
+ // this hook makes no decision, and that is the point of the docblock above.
57
+ return undefined
58
+ },
59
+
60
+ post_tool_call: (ctx) => {
61
+ const k = key(ctx.runId, ctx.name)
62
+ const startedAt = started.get(k)
63
+ started.delete(k)
64
+ console.info(
65
+ JSON.stringify({
66
+ event: 'tool.end',
67
+ tool: ctx.name,
68
+ runId: ctx.runId,
69
+ // Absent rather than zero when the start was never seen — a duration of 0ms would read as
70
+ // an instant call instead of as a missing measurement.
71
+ ms: startedAt === undefined ? undefined : Date.now() - startedAt,
72
+ }),
73
+ )
74
+ },
75
+ }