create-theokit 1.0.6 → 1.0.8
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 +5 -0
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +41 -51
- package/templates/default/CLAUDE.md +25 -0
- package/templates/default/README.md.tmpl +53 -51
- package/templates/default/_gitignore +31 -0
- package/templates/default/app/globals.css +36 -0
- package/templates/default/app/page.tsx +72 -166
- package/templates/default/app.ts +24 -0
- package/templates/default/dot-claude/rules/theokit-conventions.md +36 -0
- package/templates/default/dot-claude/settings.json +23 -0
- package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +105 -0
- package/templates/default/dot-claude/skills/theokit-config/SKILL.md +128 -0
- package/templates/default/dot-claude/skills/theokit-database/SKILL.md +112 -0
- package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +89 -0
- package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +89 -0
- package/templates/default/drizzle.config.ts +10 -0
- package/templates/default/eslint.config.mjs +5 -1
- package/templates/default/package.json.tmpl +13 -3
- package/templates/default/public/index.html +70 -0
- package/templates/default/server/agents/assistant.agent.ts +6 -2
- package/templates/default/server/controllers/tasks.controller.ts +5 -13
- package/templates/default/server/db/index.ts +21 -0
- package/templates/default/server/db/schema.ts +13 -0
- package/templates/default/server/db/seed.ts +29 -0
- package/templates/default/server/index.ts +2 -18
- package/templates/default/server/routes/tasks/[id].ts +36 -0
- package/templates/default/server/routes/tasks/index.ts +20 -0
- package/templates/default/server/toolboxes/task.tools.ts +2 -1
- package/templates/default/tests/tasks.test.ts +10 -0
- package/templates/default/theo.config.ts +0 -6
|
@@ -220,6 +220,7 @@ code, pre { font-family: var(--font-mono); }
|
|
|
220
220
|
border: 1px solid var(--border);
|
|
221
221
|
border-radius: 12px;
|
|
222
222
|
padding: 24px;
|
|
223
|
+
width: 100%;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
.card h2 {
|
|
@@ -314,6 +315,41 @@ tr.done td { opacity: 0.4; text-decoration: line-through; }
|
|
|
314
315
|
}
|
|
315
316
|
.chat-bar input:focus { border-color: var(--accent); }
|
|
316
317
|
|
|
318
|
+
/* ─── Features grid ─────────────────────────────────── */
|
|
319
|
+
|
|
320
|
+
.features {
|
|
321
|
+
margin-top: 32px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.feature {
|
|
325
|
+
padding: 20px;
|
|
326
|
+
border: 1px solid var(--border);
|
|
327
|
+
border-radius: 12px;
|
|
328
|
+
background: var(--card);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.feature h3 {
|
|
332
|
+
font-size: 14px;
|
|
333
|
+
font-weight: 600;
|
|
334
|
+
margin-bottom: 6px;
|
|
335
|
+
font-family: var(--font-mono);
|
|
336
|
+
color: var(--accent);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.feature p {
|
|
340
|
+
font-size: 13px;
|
|
341
|
+
line-height: 1.5;
|
|
342
|
+
color: var(--text-secondary);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.feature code {
|
|
346
|
+
background: var(--bg);
|
|
347
|
+
border: 1px solid var(--border);
|
|
348
|
+
padding: 1px 5px;
|
|
349
|
+
border-radius: 4px;
|
|
350
|
+
font-size: 12px;
|
|
351
|
+
}
|
|
352
|
+
|
|
317
353
|
/* ─── Footer ────────────────────────────────────────── */
|
|
318
354
|
|
|
319
355
|
.footer {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback,
|
|
3
|
+
import { useState, useEffect, useCallback, type FormEvent } from 'react'
|
|
4
4
|
|
|
5
5
|
interface Task {
|
|
6
6
|
id: number
|
|
@@ -8,30 +8,12 @@ interface Task {
|
|
|
8
8
|
priority: 'high' | 'medium' | 'low'
|
|
9
9
|
done: boolean
|
|
10
10
|
}
|
|
11
|
-
type Role = '' | 'user' | 'admin'
|
|
12
|
-
interface ChatMsg {
|
|
13
|
-
role: 'user' | 'agent' | 'tool' | 'system' | 'error'
|
|
14
|
-
text: string
|
|
15
|
-
}
|
|
16
11
|
|
|
17
12
|
export default function Page() {
|
|
18
13
|
const [tasks, setTasks] = useState<Task[]>([])
|
|
19
|
-
const [role, setRole] = useState<Role>('user')
|
|
20
14
|
const [title, setTitle] = useState('')
|
|
21
15
|
const [priority, setPriority] = useState<Task['priority']>('medium')
|
|
22
16
|
const [formError, setFormError] = useState('')
|
|
23
|
-
const [chat, setChat] = useState<ChatMsg[]>([
|
|
24
|
-
{ role: 'system', text: 'Ask me to list, create, or complete tasks...' },
|
|
25
|
-
])
|
|
26
|
-
const [chatInput, setChatInput] = useState('')
|
|
27
|
-
const [chatBusy, setChatBusy] = useState(false)
|
|
28
|
-
const chatRef = useRef<HTMLDivElement>(null)
|
|
29
|
-
|
|
30
|
-
const hdrs = useCallback((): Record<string, string> => {
|
|
31
|
-
const h: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
32
|
-
if (role) h['x-role'] = role
|
|
33
|
-
return h
|
|
34
|
-
}, [role])
|
|
35
17
|
|
|
36
18
|
const loadTasks = useCallback(async () => {
|
|
37
19
|
const res = await fetch('/api/tasks')
|
|
@@ -48,13 +30,9 @@ export default function Page() {
|
|
|
48
30
|
if (!title.trim()) return
|
|
49
31
|
const res = await fetch('/api/tasks', {
|
|
50
32
|
method: 'POST',
|
|
51
|
-
headers:
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
52
34
|
body: JSON.stringify({ title, priority }),
|
|
53
35
|
})
|
|
54
|
-
if (res.status === 403) {
|
|
55
|
-
setFormError('403 — Need User role')
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
36
|
if (!res.ok) {
|
|
59
37
|
const b = await res.json()
|
|
60
38
|
setFormError(b.error?.issues?.[0]?.message ?? `Error ${res.status}`)
|
|
@@ -64,65 +42,6 @@ export default function Page() {
|
|
|
64
42
|
loadTasks()
|
|
65
43
|
}
|
|
66
44
|
|
|
67
|
-
const sendChat = async () => {
|
|
68
|
-
const msg = chatInput.trim()
|
|
69
|
-
if (!msg || chatBusy) return
|
|
70
|
-
setChatInput('')
|
|
71
|
-
setChat((c) => [...c, { role: 'user', text: msg }])
|
|
72
|
-
setChatBusy(true)
|
|
73
|
-
try {
|
|
74
|
-
const res = await fetch('/api/agents/assistant/chat', {
|
|
75
|
-
method: 'POST',
|
|
76
|
-
headers: hdrs(),
|
|
77
|
-
body: JSON.stringify({ message: msg, sessionId: 'session-' + Date.now() }),
|
|
78
|
-
})
|
|
79
|
-
if (res.status === 403) {
|
|
80
|
-
setChat((c) => [...c, { role: 'error', text: '403 — Need User role' }])
|
|
81
|
-
return
|
|
82
|
-
}
|
|
83
|
-
const reader = res.body?.getReader()
|
|
84
|
-
if (!reader) return
|
|
85
|
-
const decoder = new TextDecoder()
|
|
86
|
-
let buf = '',
|
|
87
|
-
agentText = ''
|
|
88
|
-
while (true) {
|
|
89
|
-
const { done, value } = await reader.read()
|
|
90
|
-
if (done) break
|
|
91
|
-
buf += decoder.decode(value, { stream: true })
|
|
92
|
-
const lines = buf.split('\n')
|
|
93
|
-
buf = lines.pop() ?? ''
|
|
94
|
-
for (const line of lines) {
|
|
95
|
-
if (!line.startsWith('data: ')) continue
|
|
96
|
-
try {
|
|
97
|
-
const ev = JSON.parse(line.slice(6))
|
|
98
|
-
if (ev.type === 'text_delta') agentText += ev.content
|
|
99
|
-
else if (ev.type === 'tool_call')
|
|
100
|
-
setChat((c) => [...c, { role: 'tool', text: `🔧 ${ev.toolName}` }])
|
|
101
|
-
else if (ev.type === 'tool_result')
|
|
102
|
-
setChat((c) => [...c, { role: 'tool', text: `✅ ${(ev.output ?? '').slice(0, 80)}` }])
|
|
103
|
-
else if (ev.type === 'error')
|
|
104
|
-
setChat((c) => [...c, { role: 'error', text: ev.message }])
|
|
105
|
-
} catch {
|
|
106
|
-
/* partial */
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (agentText) setChat((c) => [...c, { role: 'agent', text: agentText }])
|
|
111
|
-
loadTasks()
|
|
112
|
-
} catch (err) {
|
|
113
|
-
setChat((c) => [
|
|
114
|
-
...c,
|
|
115
|
-
{ role: 'error', text: `Error: ${err instanceof Error ? err.message : String(err)}` },
|
|
116
|
-
])
|
|
117
|
-
} finally {
|
|
118
|
-
setChatBusy(false)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
chatRef.current?.scrollTo(0, chatRef.current.scrollHeight)
|
|
124
|
-
}, [chat])
|
|
125
|
-
|
|
126
45
|
return (
|
|
127
46
|
<div className="page">
|
|
128
47
|
<div className="main">
|
|
@@ -150,94 +69,81 @@ export default function Page() {
|
|
|
150
69
|
</a>
|
|
151
70
|
</nav>
|
|
152
71
|
<p className="hint">
|
|
153
|
-
Edit <code>app/page.tsx</code> to get started.
|
|
72
|
+
Edit <code>app/page.tsx</code> to get started. Changes hot-reload instantly.
|
|
154
73
|
</p>
|
|
155
74
|
</header>
|
|
156
75
|
|
|
157
|
-
{/*
|
|
158
|
-
<
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
<
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
<
|
|
178
|
-
|
|
76
|
+
{/* Tasks */}
|
|
77
|
+
<section className="card">
|
|
78
|
+
<h2>
|
|
79
|
+
Tasks <span className="badge">defineRoute + Drizzle</span>
|
|
80
|
+
</h2>
|
|
81
|
+
<table>
|
|
82
|
+
<thead>
|
|
83
|
+
<tr>
|
|
84
|
+
<th>Task</th>
|
|
85
|
+
<th>Priority</th>
|
|
86
|
+
<th>Status</th>
|
|
87
|
+
</tr>
|
|
88
|
+
</thead>
|
|
89
|
+
<tbody>
|
|
90
|
+
{tasks.map((t) => (
|
|
91
|
+
<tr key={t.id} className={t.done ? 'done' : ''}>
|
|
92
|
+
<td>
|
|
93
|
+
{t.done ? '✅ ' : '○ '}
|
|
94
|
+
{t.title}
|
|
95
|
+
</td>
|
|
96
|
+
<td>
|
|
97
|
+
<span className={`prio prio-${t.priority}`}>{t.priority}</span>
|
|
98
|
+
</td>
|
|
99
|
+
<td>{t.done ? 'Done' : 'To do'}</td>
|
|
179
100
|
</tr>
|
|
180
|
-
</thead>
|
|
181
|
-
<tbody>
|
|
182
|
-
{tasks.map((t) => (
|
|
183
|
-
<tr key={t.id} className={t.done ? 'done' : ''}>
|
|
184
|
-
<td>
|
|
185
|
-
{t.done ? '✅ ' : '○ '}
|
|
186
|
-
{t.title}
|
|
187
|
-
</td>
|
|
188
|
-
<td>
|
|
189
|
-
<span className={`prio prio-${t.priority}`}>{t.priority}</span>
|
|
190
|
-
</td>
|
|
191
|
-
<td>{t.done ? 'Done' : 'To do'}</td>
|
|
192
|
-
</tr>
|
|
193
|
-
))}
|
|
194
|
-
</tbody>
|
|
195
|
-
</table>
|
|
196
|
-
<form onSubmit={createTask} className="create-bar">
|
|
197
|
-
<input
|
|
198
|
-
value={title}
|
|
199
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
200
|
-
placeholder="New task..."
|
|
201
|
-
required
|
|
202
|
-
minLength={3}
|
|
203
|
-
/>
|
|
204
|
-
<select
|
|
205
|
-
value={priority}
|
|
206
|
-
onChange={(e) => setPriority(e.target.value as Task['priority'])}
|
|
207
|
-
>
|
|
208
|
-
<option value="medium">Medium</option>
|
|
209
|
-
<option value="high">High</option>
|
|
210
|
-
<option value="low">Low</option>
|
|
211
|
-
</select>
|
|
212
|
-
<button type="submit">Add</button>
|
|
213
|
-
</form>
|
|
214
|
-
{formError && <p className="error">{formError}</p>}
|
|
215
|
-
</section>
|
|
216
|
-
|
|
217
|
-
<section className="card">
|
|
218
|
-
<h2>
|
|
219
|
-
AI Assistant <span className="badge badge-ai">@Agent + SSE</span>
|
|
220
|
-
</h2>
|
|
221
|
-
<div ref={chatRef} className="chat-box">
|
|
222
|
-
{chat.map((m, i) => (
|
|
223
|
-
<div key={i} className={`msg ${m.role}`}>
|
|
224
|
-
{m.role === 'user' ? `You: ${m.text}` : m.text}
|
|
225
|
-
</div>
|
|
226
101
|
))}
|
|
227
|
-
</
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
102
|
+
</tbody>
|
|
103
|
+
</table>
|
|
104
|
+
<form onSubmit={createTask} className="create-bar">
|
|
105
|
+
<input
|
|
106
|
+
value={title}
|
|
107
|
+
onChange={(e) => setTitle(e.target.value)}
|
|
108
|
+
placeholder="New task..."
|
|
109
|
+
required
|
|
110
|
+
minLength={3}
|
|
111
|
+
/>
|
|
112
|
+
<select
|
|
113
|
+
value={priority}
|
|
114
|
+
onChange={(e) => setPriority(e.target.value as Task['priority'])}
|
|
115
|
+
>
|
|
116
|
+
<option value="medium">Medium</option>
|
|
117
|
+
<option value="high">High</option>
|
|
118
|
+
<option value="low">Low</option>
|
|
119
|
+
</select>
|
|
120
|
+
<button type="submit">Add</button>
|
|
121
|
+
</form>
|
|
122
|
+
{formError && <p className="error">{formError}</p>}
|
|
123
|
+
</section>
|
|
124
|
+
|
|
125
|
+
{/* Features */}
|
|
126
|
+
<div className="grid features">
|
|
127
|
+
<div className="feature">
|
|
128
|
+
<h3>defineRoute</h3>
|
|
129
|
+
<p>
|
|
130
|
+
Typed API routes with Zod validation. See <code>server/routes/tasks/</code>
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="feature">
|
|
134
|
+
<h3>Drizzle + SQLite</h3>
|
|
135
|
+
<p>
|
|
136
|
+
Type-safe database with zero config. Schema in <code>server/db/schema.ts</code>
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="feature">
|
|
140
|
+
<h3>@Agent + @Tool</h3>
|
|
141
|
+
<p>AI agents with SSE streaming, budget control, and human-in-the-loop approval.</p>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="feature">
|
|
144
|
+
<h3>React + Vite</h3>
|
|
145
|
+
<p>File-based routing, HMR, SSR streaming. Edit and see changes instantly.</p>
|
|
146
|
+
</div>
|
|
241
147
|
</div>
|
|
242
148
|
|
|
243
149
|
{/* Footer */}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TheoKit App — convention over configuration.
|
|
3
|
+
*
|
|
4
|
+
* "The framework that reduces noise for humans + AI."
|
|
5
|
+
*
|
|
6
|
+
* Backend classes registered in server/index.ts (one barrel, like Rails).
|
|
7
|
+
* Routes inferred from class names. Zero manual wiring.
|
|
8
|
+
*/
|
|
9
|
+
import 'reflect-metadata'
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { TheoApp } from '@theokit/http/app'
|
|
12
|
+
import { TasksController, AssistantAgent, TaskTools } from './server/index.js'
|
|
13
|
+
|
|
14
|
+
let html: string | undefined
|
|
15
|
+
try { html = readFileSync(new URL('./public/index.html', import.meta.url), 'utf-8') } catch { /* no frontend */ }
|
|
16
|
+
|
|
17
|
+
const app = await TheoApp.create({
|
|
18
|
+
controllers: [TasksController],
|
|
19
|
+
agents: [AssistantAgent],
|
|
20
|
+
providers: [TaskTools],
|
|
21
|
+
html,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
await app.listen(3000)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# TheoKit Conventions
|
|
2
|
+
|
|
3
|
+
## Imports
|
|
4
|
+
|
|
5
|
+
- Use `theokit/server/define` for defineRoute, defineAction, defineWebSocket
|
|
6
|
+
- Use `theokit/client` for theoFetch, createAppClient
|
|
7
|
+
- Use `theokit/server/auth` for session/auth APIs
|
|
8
|
+
- NEVER import from `theokit/dist/...` or `theokit/src/...`
|
|
9
|
+
- NEVER import internal modules: `theokit/core`, `theokit/vite-plugin`, `theokit/adapters/*`
|
|
10
|
+
|
|
11
|
+
## Validation
|
|
12
|
+
|
|
13
|
+
- Zod is the single source of truth for types and validation
|
|
14
|
+
- Define schema ONCE with `z.object(...)`, derive types with `z.infer<>`
|
|
15
|
+
- NEVER duplicate a Zod schema as a manual TypeScript interface
|
|
16
|
+
- NEVER parse request body manually — use `body:` in defineRoute
|
|
17
|
+
|
|
18
|
+
## Routes
|
|
19
|
+
|
|
20
|
+
- File at `server/routes/tasks/[id].ts` maps to `/api/tasks/:id`
|
|
21
|
+
- Export HTTP method handlers: `export const GET = defineRoute({...})`
|
|
22
|
+
- Use `params: z.object({...})` for URL params, `body:` for request body
|
|
23
|
+
- Use `status: 201` for creation responses, not manual `res.status()`
|
|
24
|
+
|
|
25
|
+
## Types
|
|
26
|
+
|
|
27
|
+
- No `any` in production code
|
|
28
|
+
- No `@ts-ignore` or `@ts-expect-error`
|
|
29
|
+
- No `as` type assertions — use Zod schemas or type guards
|
|
30
|
+
|
|
31
|
+
## Database
|
|
32
|
+
|
|
33
|
+
- Schema lives in `server/db/schema.ts` (Drizzle ORM)
|
|
34
|
+
- Connection in `server/db/index.ts`
|
|
35
|
+
- Seeds in `server/db/seed.ts`
|
|
36
|
+
- Use `npx drizzle-kit push` for dev, `npx drizzle-kit generate` for prod migrations
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npm run *)",
|
|
5
|
+
"Bash(npx theokit *)",
|
|
6
|
+
"Bash(npx vitest *)",
|
|
7
|
+
"Bash(npx tsc *)",
|
|
8
|
+
"Bash(npx eslint *)",
|
|
9
|
+
"Bash(npx drizzle-kit *)",
|
|
10
|
+
"Bash(git status)",
|
|
11
|
+
"Bash(git diff *)",
|
|
12
|
+
"Bash(git log *)"
|
|
13
|
+
],
|
|
14
|
+
"deny": [
|
|
15
|
+
"Read(.env*)",
|
|
16
|
+
"Read(**/.env*)",
|
|
17
|
+
"Bash(sudo *)",
|
|
18
|
+
"Bash(rm -rf *)",
|
|
19
|
+
"Bash(git push --force *)",
|
|
20
|
+
"Bash(git reset --hard *)"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: theokit-agents
|
|
3
|
+
description: TheoKit agent/LLM integration — @Agent, @Tool, @Toolbox decorators, streaming, memory
|
|
4
|
+
user-invocable: false
|
|
5
|
+
paths:
|
|
6
|
+
- "**/*agent*"
|
|
7
|
+
- "**/*Agent*"
|
|
8
|
+
- "**/*tool*"
|
|
9
|
+
- "**/*Tool*"
|
|
10
|
+
- "**/*toolbox*"
|
|
11
|
+
- "**/*Toolbox*"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# TheoKit Agents & Tools
|
|
15
|
+
|
|
16
|
+
## @Agent Decorator
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
import { Agent, MainLoop, Hook, Memory, Budget } from '@theokit/agents'
|
|
20
|
+
|
|
21
|
+
@Agent({
|
|
22
|
+
model: 'openai/gpt-4o-mini', // Required: LLM model
|
|
23
|
+
systemPrompt: 'You are a helpful task assistant.',
|
|
24
|
+
})
|
|
25
|
+
@Memory({ provider: 'built-in', scope: 'per-user' }) // Optional
|
|
26
|
+
@Budget({ maxCostUsd: 1.00, window: 'daily' }) // Optional
|
|
27
|
+
export class AssistantAgent {
|
|
28
|
+
@MainLoop({ strategy: 'react', maxIterations: 5 })
|
|
29
|
+
async run() {
|
|
30
|
+
// Framework handles the LLM loop
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Hook('before:llm-call')
|
|
34
|
+
async onBeforeLLM(ctx) {
|
|
35
|
+
// Intercept before each LLM call
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Convention: `AssistantAgent` class name maps to `GET/POST /api/agents/assistant`.
|
|
41
|
+
|
|
42
|
+
## @Tool Decorator
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { Toolbox, Tool } from '@theokit/agents'
|
|
46
|
+
import { z } from 'zod'
|
|
47
|
+
|
|
48
|
+
@Toolbox()
|
|
49
|
+
export class TaskTools {
|
|
50
|
+
@Tool({
|
|
51
|
+
name: 'list_tasks',
|
|
52
|
+
description: 'List all tasks, optionally filtered by status',
|
|
53
|
+
input: z.object({
|
|
54
|
+
done: z.boolean().optional(),
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
57
|
+
async listTasks({ done }: { done?: boolean }) {
|
|
58
|
+
const all = db.select().from(tasks).all()
|
|
59
|
+
return done !== undefined ? all.filter(t => t.done === done) : all
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Tool({
|
|
63
|
+
name: 'create_task',
|
|
64
|
+
description: 'Create a new task with a title',
|
|
65
|
+
input: z.object({
|
|
66
|
+
title: z.string().min(1),
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
async createTask({ title }: { title: string }) {
|
|
70
|
+
return db.insert(tasks).values({ title }).returning().get()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Frontend — useAgentStream
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { useAgentStream } from 'theokit/client'
|
|
79
|
+
|
|
80
|
+
function ChatUI() {
|
|
81
|
+
const { status, events, send } = useAgentStream('/api/agents/assistant')
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
{events.map(e => <p key={e.id}>{e.content}</p>)}
|
|
86
|
+
<button onClick={() => send({ message: 'Hello' })}>Send</button>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Rules
|
|
93
|
+
|
|
94
|
+
- Tool `name` and `description` are ALWAYS explicit — never inferred from method names (G4)
|
|
95
|
+
- Tool `input` uses Zod schema — same pattern as defineRoute
|
|
96
|
+
- `@UseGuards()` works on agents (shared with HTTP pipeline)
|
|
97
|
+
- `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings, not enforced at runtime)
|
|
98
|
+
- Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
|
|
99
|
+
|
|
100
|
+
## Anti-patterns
|
|
101
|
+
|
|
102
|
+
- NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent + @Tool
|
|
103
|
+
- NEVER reimplement tool calling loop — the SDK handles it
|
|
104
|
+
- NEVER store conversations manually — use @Memory
|
|
105
|
+
- NEVER infer tool capability from method name — always provide explicit `name` + `description`
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: theokit-config
|
|
3
|
+
description: TheoKit configuration — defineConfig, plugins, security, storage, agents, build targets
|
|
4
|
+
user-invocable: false
|
|
5
|
+
paths:
|
|
6
|
+
- "theo.config*"
|
|
7
|
+
- "**/*config*"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# TheoKit Configuration
|
|
11
|
+
|
|
12
|
+
## theo.config.ts
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { defineConfig } from 'theokit'
|
|
16
|
+
|
|
17
|
+
export default defineConfig({
|
|
18
|
+
// Basic
|
|
19
|
+
name: 'my-app', // DNS-1123 format (lowercase + hyphens)
|
|
20
|
+
port: 3000, // Dev + production port
|
|
21
|
+
|
|
22
|
+
// SSR (default: false)
|
|
23
|
+
ssr: false,
|
|
24
|
+
|
|
25
|
+
// Security (defaults are secure)
|
|
26
|
+
security: {
|
|
27
|
+
csrf: true, // CSRF protection (default: true)
|
|
28
|
+
csp: 'report-only', // Content Security Policy
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Agent runtime
|
|
32
|
+
agents: {
|
|
33
|
+
maxRegistries: 100,
|
|
34
|
+
registry: {
|
|
35
|
+
maxAgents: 100,
|
|
36
|
+
idleTimeoutMs: 30 * 60_000,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
// DevTools overlay (dev only)
|
|
41
|
+
devtools: true,
|
|
42
|
+
|
|
43
|
+
// Plugins
|
|
44
|
+
plugins: [],
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Common Configuration Patterns
|
|
49
|
+
|
|
50
|
+
### Adding CORS
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { defineConfig } from 'theokit'
|
|
54
|
+
|
|
55
|
+
export default defineConfig({
|
|
56
|
+
// CORS is handled by the framework — configure in route-level or globally
|
|
57
|
+
security: {
|
|
58
|
+
cors: {
|
|
59
|
+
origin: ['http://localhost:3000', 'https://myapp.com'],
|
|
60
|
+
credentials: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Storage (Postgres + Redis)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
storage: {
|
|
71
|
+
postgres: [{ url: process.env.DATABASE_URL }],
|
|
72
|
+
redis: [{ url: process.env.REDIS_URL }],
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Rate Limiting
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export default defineConfig({
|
|
81
|
+
rateLimit: {
|
|
82
|
+
global: { max: 100, windowMs: 60_000 },
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### OpenAPI Generation
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
openapi: {
|
|
92
|
+
title: 'My App API',
|
|
93
|
+
version: '1.0.0',
|
|
94
|
+
outDir: 'docs/api',
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## CLI Commands
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx theokit dev # Start dev server with HMR
|
|
103
|
+
npx theokit build # Build for Node.js
|
|
104
|
+
npx theokit build --target=node # Explicit target
|
|
105
|
+
npx theokit start # Run production build
|
|
106
|
+
npx theokit routes # List all discovered endpoints
|
|
107
|
+
npx theokit generate route tasks # Scaffold a new route
|
|
108
|
+
npx theokit generate resource posts title:string # Scaffold CRUD resource
|
|
109
|
+
npx theokit db migrate # Run database migrations
|
|
110
|
+
npx theokit db seed # Seed database
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Environment Variables
|
|
114
|
+
|
|
115
|
+
- `PORT` — Server port (overrides config)
|
|
116
|
+
- `HOST` — Server host
|
|
117
|
+
- `NODE_ENV` — `development` | `production`
|
|
118
|
+
- `DATABASE_URL` — Postgres connection string (when using postgres storage)
|
|
119
|
+
- `REDIS_URL` — Redis connection string (when using redis storage)
|
|
120
|
+
|
|
121
|
+
Env vars are loaded from `.env` (dev) and `.env.production` (build). NEVER commit `.env` files.
|
|
122
|
+
|
|
123
|
+
## Anti-patterns
|
|
124
|
+
|
|
125
|
+
- NEVER hardcode secrets in theo.config.ts — use environment variables
|
|
126
|
+
- NEVER set `security.csrf: false` in production
|
|
127
|
+
- NEVER use `ssr: true` without understanding hydration (start with `false`)
|
|
128
|
+
- NEVER add plugins that don't match `defineTheoPlugin` interface
|