create-theokit 1.0.9 → 1.0.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/package.json +1 -1
- package/templates/default/AGENTS.md +19 -14
- package/templates/default/app/page.tsx +1 -100
- package/templates/default/app.ts +11 -20
- package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +63 -36
- package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +30 -5
- package/templates/default/package.json.tmpl +0 -2
- package/templates/default/server/db/index.ts +0 -11
- package/templates/default/server/db/schema.ts +13 -13
- package/templates/default/tests/tasks.test.ts +12 -32
- package/templates/default/server/agents/assistant.agent.ts +0 -43
- package/templates/default/server/controllers/tasks.controller.ts +0 -70
- package/templates/default/server/db/seed.ts +0 -29
- package/templates/default/server/filters/http-error.filter.ts +0 -20
- package/templates/default/server/guards/auth.guard.ts +0 -47
- package/templates/default/server/interceptors/timing.interceptor.ts +0 -14
- package/templates/default/server/middleware/logger.middleware.ts +0 -12
- package/templates/default/server/routes/tasks/[id].ts +0 -36
- package/templates/default/server/routes/tasks/index.ts +0 -20
- package/templates/default/server/store.ts +0 -46
- package/templates/default/server/toolboxes/task.tools.ts +0 -58
package/package.json
CHANGED
|
@@ -10,18 +10,14 @@ This is a **full-stack TypeScript app** built with TheoKit — a framework for A
|
|
|
10
10
|
server/
|
|
11
11
|
routes/ → HTTP API routes (defineRoute + Zod validation)
|
|
12
12
|
health.ts → GET /api/health
|
|
13
|
-
tasks/
|
|
14
|
-
index.ts → GET /api/tasks (list) + POST /api/tasks (create)
|
|
15
|
-
[id].ts → GET/PUT/DELETE /api/tasks/:id
|
|
16
13
|
db/
|
|
17
|
-
schema.ts → Drizzle ORM schema (SQLite)
|
|
18
|
-
index.ts → DB connection
|
|
19
|
-
seed.ts → Seed data (run with `npm run seed`)
|
|
14
|
+
schema.ts → Drizzle ORM schema (SQLite) — empty, add your tables
|
|
15
|
+
index.ts → DB connection (better-sqlite3, WAL mode)
|
|
20
16
|
app/
|
|
21
17
|
page.tsx → React frontend
|
|
22
18
|
layout.tsx → Root layout
|
|
23
19
|
tests/
|
|
24
|
-
tasks.test.ts → Example
|
|
20
|
+
tasks.test.ts → Example unit test
|
|
25
21
|
```
|
|
26
22
|
|
|
27
23
|
## Key Patterns
|
|
@@ -32,13 +28,13 @@ import { defineRoute } from 'theokit/server/define'
|
|
|
32
28
|
import { z } from 'zod'
|
|
33
29
|
|
|
34
30
|
export const GET = defineRoute({
|
|
35
|
-
handler: () => db.select().from(
|
|
31
|
+
handler: () => db.select().from(posts).all(),
|
|
36
32
|
})
|
|
37
33
|
|
|
38
34
|
export const POST = defineRoute({
|
|
39
35
|
body: z.object({ title: z.string().min(3) }),
|
|
40
36
|
status: 201,
|
|
41
|
-
handler: ({ body }) => db.insert(
|
|
37
|
+
handler: ({ body }) => db.insert(posts).values(body).returning().get(),
|
|
42
38
|
})
|
|
43
39
|
```
|
|
44
40
|
|
|
@@ -46,25 +42,30 @@ export const POST = defineRoute({
|
|
|
46
42
|
```typescript
|
|
47
43
|
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
48
44
|
|
|
49
|
-
export const
|
|
45
|
+
export const posts = sqliteTable('posts', {
|
|
50
46
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
51
47
|
title: text('title').notNull(),
|
|
52
|
-
|
|
48
|
+
published: integer('published', { mode: 'boolean' }).notNull().default(false),
|
|
53
49
|
})
|
|
54
50
|
```
|
|
55
51
|
|
|
52
|
+
### Scaffold a Resource
|
|
53
|
+
```bash
|
|
54
|
+
npx theokit generate resource posts title:string published:boolean
|
|
55
|
+
# Creates: schema table + routes (CRUD) + test
|
|
56
|
+
```
|
|
57
|
+
|
|
56
58
|
### Validation
|
|
57
59
|
- **Zod is the single source of truth** — define schema once, get types + validation + OpenAPI
|
|
58
60
|
- `body: z.object(...)` in defineRoute validates automatically, returns 422 on failure
|
|
59
61
|
- Use `z.infer<typeof schema>` for TypeScript types
|
|
60
62
|
|
|
61
63
|
### Dynamic Routes
|
|
62
|
-
- `server/routes/
|
|
64
|
+
- `server/routes/posts/[id].ts` → `/api/posts/:id`
|
|
63
65
|
- Params validated with `params: z.object({ id: z.coerce.number() })`
|
|
64
66
|
|
|
65
67
|
### Path Aliases
|
|
66
68
|
- `@/*` → project root (configured in tsconfig.json)
|
|
67
|
-
- `@/server/*` → `./server/*`
|
|
68
69
|
|
|
69
70
|
## Commands
|
|
70
71
|
|
|
@@ -73,10 +74,12 @@ npm run dev # Start dev server
|
|
|
73
74
|
npm run build # Build for production
|
|
74
75
|
npm run start # Run production build
|
|
75
76
|
npm run test # Run tests (vitest)
|
|
76
|
-
npm run seed # Seed database with sample data
|
|
77
77
|
npm run lint # ESLint check
|
|
78
78
|
npm run format # Prettier format
|
|
79
79
|
npm run typecheck # TypeScript type check
|
|
80
|
+
npx theokit generate resource <name> <fields...> # Scaffold CRUD resource
|
|
81
|
+
npx drizzle-kit push # Apply schema changes (dev)
|
|
82
|
+
npx drizzle-kit generate # Generate migration files (prod)
|
|
80
83
|
```
|
|
81
84
|
|
|
82
85
|
## Don't
|
|
@@ -84,3 +87,5 @@ npm run typecheck # TypeScript type check
|
|
|
84
87
|
- Don't use `any` — use Zod schemas + `z.infer<>`
|
|
85
88
|
- Don't write raw `res.status().json()` — use defineRoute with status option
|
|
86
89
|
- Don't parse request body manually — use `body: z.object(...)` in defineRoute
|
|
90
|
+
- Don't import from `theokit/dist/...` or `theokit/src/...` — use public exports only
|
|
91
|
+
- Don't call LLM APIs directly — use @Agent + @Tool decorators
|
|
@@ -1,51 +1,7 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback, type FormEvent } from 'react'
|
|
4
|
-
|
|
5
|
-
interface Task {
|
|
6
|
-
id: number
|
|
7
|
-
title: string
|
|
8
|
-
priority: 'high' | 'medium' | 'low'
|
|
9
|
-
done: boolean
|
|
10
|
-
}
|
|
11
|
-
|
|
12
1
|
export default function Page() {
|
|
13
|
-
const [tasks, setTasks] = useState<Task[]>([])
|
|
14
|
-
const [title, setTitle] = useState('')
|
|
15
|
-
const [priority, setPriority] = useState<Task['priority']>('medium')
|
|
16
|
-
const [formError, setFormError] = useState('')
|
|
17
|
-
|
|
18
|
-
const loadTasks = useCallback(async () => {
|
|
19
|
-
const res = await fetch('/api/tasks')
|
|
20
|
-
if (res.ok) setTasks(await res.json())
|
|
21
|
-
}, [])
|
|
22
|
-
|
|
23
|
-
useEffect(() => {
|
|
24
|
-
loadTasks()
|
|
25
|
-
}, [loadTasks])
|
|
26
|
-
|
|
27
|
-
const createTask = async (e: FormEvent) => {
|
|
28
|
-
e.preventDefault()
|
|
29
|
-
setFormError('')
|
|
30
|
-
if (!title.trim()) return
|
|
31
|
-
const res = await fetch('/api/tasks', {
|
|
32
|
-
method: 'POST',
|
|
33
|
-
headers: { 'Content-Type': 'application/json' },
|
|
34
|
-
body: JSON.stringify({ title, priority }),
|
|
35
|
-
})
|
|
36
|
-
if (!res.ok) {
|
|
37
|
-
const b = await res.json()
|
|
38
|
-
setFormError(b.error?.issues?.[0]?.message ?? `Error ${res.status}`)
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
setTitle('')
|
|
42
|
-
loadTasks()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
2
|
return (
|
|
46
3
|
<div className="page">
|
|
47
4
|
<div className="main">
|
|
48
|
-
{/* Hero */}
|
|
49
5
|
<header className="hero">
|
|
50
6
|
<img src="/logo.png" alt="TheoKit" width={72} height={72} className="hero-logo" />
|
|
51
7
|
<h1>TheoKit</h1>
|
|
@@ -73,61 +29,11 @@ export default function Page() {
|
|
|
73
29
|
</p>
|
|
74
30
|
</header>
|
|
75
31
|
|
|
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>
|
|
100
|
-
</tr>
|
|
101
|
-
))}
|
|
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
32
|
<div className="grid features">
|
|
127
33
|
<div className="feature">
|
|
128
34
|
<h3>defineRoute</h3>
|
|
129
35
|
<p>
|
|
130
|
-
Typed API routes with Zod validation. See <code>server/routes
|
|
36
|
+
Typed API routes with Zod validation. See <code>server/routes/</code>
|
|
131
37
|
</p>
|
|
132
38
|
</div>
|
|
133
39
|
<div className="feature">
|
|
@@ -146,7 +52,6 @@ export default function Page() {
|
|
|
146
52
|
</div>
|
|
147
53
|
</div>
|
|
148
54
|
|
|
149
|
-
{/* Footer */}
|
|
150
55
|
<footer className="footer">
|
|
151
56
|
Powered by{' '}
|
|
152
57
|
<a href="https://usetheo.dev" target="_blank" rel="noopener noreferrer">
|
|
@@ -156,10 +61,6 @@ export default function Page() {
|
|
|
156
61
|
<a href="https://github.com/usetheodev/theokit" target="_blank" rel="noopener noreferrer">
|
|
157
62
|
GitHub
|
|
158
63
|
</a>
|
|
159
|
-
{' · '}
|
|
160
|
-
<a href="https://discord.usetheo.dev" target="_blank" rel="noopener noreferrer">
|
|
161
|
-
Discord
|
|
162
|
-
</a>
|
|
163
64
|
</footer>
|
|
164
65
|
</div>
|
|
165
66
|
</div>
|
package/templates/default/app.ts
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TheoKit App —
|
|
2
|
+
* TheoKit App — manual bootstrap (optional).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* `theokit dev` auto-discovers server/routes/ — you don't need this file
|
|
5
|
+
* for basic usage. Use it when you need explicit controller/agent registration.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* Example with controllers + agents:
|
|
8
|
+
*
|
|
9
|
+
* import 'reflect-metadata'
|
|
10
|
+
* import { TheoApp } from '@theokit/http/app'
|
|
11
|
+
* import { MyController } from './server/controllers/my.controller.js'
|
|
12
|
+
*
|
|
13
|
+
* const app = await TheoApp.create({ controllers: [MyController] })
|
|
14
|
+
* await app.listen(3000)
|
|
8
15
|
*/
|
|
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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: theokit-agents
|
|
3
|
-
description: TheoKit agent/LLM integration —
|
|
3
|
+
description: TheoKit agent/LLM integration — two streaming surfaces (decorator vs manual), @Tool, @Toolbox, memory
|
|
4
4
|
user-invocable: false
|
|
5
5
|
paths:
|
|
6
6
|
- "**/*agent*"
|
|
@@ -13,31 +13,51 @@ paths:
|
|
|
13
13
|
|
|
14
14
|
# TheoKit Agents & Tools
|
|
15
15
|
|
|
16
|
-
##
|
|
16
|
+
## Two Streaming Surfaces — pick one per endpoint
|
|
17
|
+
|
|
18
|
+
TheoKit ships two ways to create agent endpoints. Use ONE per endpoint, not both.
|
|
19
|
+
|
|
20
|
+
| Surface | Server | Events | Client | When to use |
|
|
21
|
+
|---------|--------|--------|--------|-------------|
|
|
22
|
+
| **Manual** (recommended for most apps) | `defineAgentEndpoint({ handler: async function* })` | `AgentEvent` (Message, ToolCall, Result, Error) | `useAgentStream()` / `consumeAgentStream()` | Full control over the LLM loop — you write the generator |
|
|
23
|
+
| **Decorator** (`@theokit/agents`) | `@Agent` class → auto-generated route | `AgentStreamEvent` (TextDelta, ToolCall, Done...) | (same client hooks work) | Declarative — framework manages LLM loop via `@MainLoop` |
|
|
24
|
+
|
|
25
|
+
### Surface 1: Manual (defineAgentEndpoint)
|
|
17
26
|
|
|
18
27
|
```typescript
|
|
19
|
-
|
|
28
|
+
// server/routes/agents/assistant.ts
|
|
29
|
+
import { defineAgentEndpoint } from 'theokit/server/define'
|
|
30
|
+
import type { AgentEvent } from 'theokit'
|
|
31
|
+
|
|
32
|
+
export const POST = defineAgentEndpoint({
|
|
33
|
+
handler: async function* ({ body }): AsyncGenerator<AgentEvent> {
|
|
34
|
+
// You control the LLM loop
|
|
35
|
+
yield { type: 'message', content: 'Thinking...' }
|
|
36
|
+
const result = await callLLM(body.message)
|
|
37
|
+
yield { type: 'message', content: result }
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Surface 2: Decorator (@Agent)
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// server/agents/assistant.agent.ts
|
|
46
|
+
import { Agent, MainLoop, Tool, Toolbox } from '@theokit/agents'
|
|
20
47
|
|
|
21
48
|
@Agent({
|
|
22
|
-
model: 'openai/gpt-4o-mini',
|
|
23
|
-
systemPrompt: 'You are a helpful
|
|
49
|
+
model: 'openai/gpt-4o-mini',
|
|
50
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
24
51
|
})
|
|
25
|
-
@Memory({ provider: 'built-in', scope: 'per-user' }) // Optional
|
|
26
|
-
@Budget({ maxCostUsd: 1.00, window: 'daily' }) // Optional
|
|
27
52
|
export class AssistantAgent {
|
|
28
53
|
@MainLoop({ strategy: 'react', maxIterations: 5 })
|
|
29
54
|
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
|
|
55
|
+
// Framework handles the LLM loop via @theokit/sdk
|
|
36
56
|
}
|
|
37
57
|
}
|
|
38
58
|
```
|
|
39
59
|
|
|
40
|
-
Convention: `AssistantAgent`
|
|
60
|
+
Convention: `AssistantAgent` → `POST /api/agents/assistant`
|
|
41
61
|
|
|
42
62
|
## @Tool Decorator
|
|
43
63
|
|
|
@@ -50,56 +70,63 @@ export class TaskTools {
|
|
|
50
70
|
@Tool({
|
|
51
71
|
name: 'list_tasks',
|
|
52
72
|
description: 'List all tasks, optionally filtered by status',
|
|
53
|
-
input: z.object({
|
|
54
|
-
done: z.boolean().optional(),
|
|
55
|
-
}),
|
|
73
|
+
input: z.object({ done: z.boolean().optional() }),
|
|
56
74
|
})
|
|
57
75
|
async listTasks({ done }: { done?: boolean }) {
|
|
58
|
-
|
|
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()
|
|
76
|
+
return db.select().from(tasks).all()
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
```
|
|
74
80
|
|
|
75
|
-
##
|
|
81
|
+
## Client — useAgentStream (React hook)
|
|
82
|
+
|
|
83
|
+
Works with BOTH surfaces. Transport: `fetch` POST + `ReadableStream` (SSE).
|
|
76
84
|
|
|
77
85
|
```typescript
|
|
78
86
|
import { useAgentStream } from 'theokit/client'
|
|
79
87
|
|
|
80
88
|
function ChatUI() {
|
|
81
|
-
const { status, events, send } = useAgentStream('/api/agents/assistant')
|
|
89
|
+
const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
|
|
82
90
|
|
|
83
91
|
return (
|
|
84
92
|
<div>
|
|
85
|
-
{
|
|
93
|
+
{status === 'streaming' && <p>Thinking...</p>}
|
|
94
|
+
{events.map(e => (
|
|
95
|
+
<div key={e.id}>
|
|
96
|
+
{e.type === 'message' && <p>{e.content}</p>}
|
|
97
|
+
{e.type === 'tool_call' && <p>Using tool: {e.name}</p>}
|
|
98
|
+
</div>
|
|
99
|
+
))}
|
|
86
100
|
<button onClick={() => send({ message: 'Hello' })}>Send</button>
|
|
87
101
|
</div>
|
|
88
102
|
)
|
|
89
103
|
}
|
|
90
104
|
```
|
|
91
105
|
|
|
106
|
+
### Non-React: consumeAgentStream
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { consumeAgentStream } from 'theokit/client'
|
|
110
|
+
|
|
111
|
+
const stream = consumeAgentStream('/api/agents/assistant', { body: { message: 'Hi' } })
|
|
112
|
+
for await (const event of stream) {
|
|
113
|
+
console.log(event.type, event.content)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
92
117
|
## Rules
|
|
93
118
|
|
|
94
119
|
- Tool `name` and `description` are ALWAYS explicit — never inferred from method names (G4)
|
|
95
120
|
- Tool `input` uses Zod schema — same pattern as defineRoute
|
|
96
121
|
- `@UseGuards()` works on agents (shared with HTTP pipeline)
|
|
97
|
-
- `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings
|
|
122
|
+
- `@UseInterceptors()` and `@UseFilters()` on agents are metadata-only (emit warnings)
|
|
98
123
|
- Agent runtime is `@theokit/sdk` — NEVER call LLM APIs directly via fetch
|
|
124
|
+
- Pick ONE surface per endpoint — don't mix defineAgentEndpoint with @Agent for the same route
|
|
99
125
|
|
|
100
126
|
## Anti-patterns
|
|
101
127
|
|
|
102
|
-
- NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent
|
|
128
|
+
- NEVER call OpenAI/Anthropic/OpenRouter APIs directly — use @Agent or defineAgentEndpoint
|
|
103
129
|
- NEVER reimplement tool calling loop — the SDK handles it
|
|
104
|
-
- NEVER store conversations manually — use @Memory
|
|
130
|
+
- NEVER store conversations manually — use @Memory (decorator) or SDK persistence
|
|
105
131
|
- NEVER infer tool capability from method name — always provide explicit `name` + `description`
|
|
132
|
+
- NEVER mix both surfaces for the same endpoint — pick manual OR decorator
|
|
@@ -43,18 +43,21 @@ import { createAppClient } from 'theokit/client'
|
|
|
43
43
|
|
|
44
44
|
const client = createAppClient()
|
|
45
45
|
|
|
46
|
-
// Proxy pattern — method names match route structure
|
|
47
46
|
const tasks = await client.tasks.GET()
|
|
48
47
|
const task = await client.tasks[':id'].GET({ params: { id: 1 } })
|
|
49
48
|
const created = await client.tasks.POST({ body: { title: 'New' } })
|
|
50
49
|
```
|
|
51
50
|
|
|
52
|
-
## Agent Streaming
|
|
51
|
+
## Agent Streaming
|
|
52
|
+
|
|
53
|
+
Three client APIs, all from `theokit/client`:
|
|
54
|
+
|
|
55
|
+
### useAgentStream (React hook — most common)
|
|
53
56
|
|
|
54
57
|
```typescript
|
|
55
58
|
import { useAgentStream } from 'theokit/client'
|
|
56
59
|
|
|
57
|
-
function
|
|
60
|
+
function ChatUI() {
|
|
58
61
|
const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
|
|
59
62
|
|
|
60
63
|
return (
|
|
@@ -66,12 +69,34 @@ function ChatComponent() {
|
|
|
66
69
|
{event.type === 'tool_call' && <p>Using tool: {event.name}</p>}
|
|
67
70
|
</div>
|
|
68
71
|
))}
|
|
69
|
-
<
|
|
72
|
+
<button onClick={() => send({ message: 'Hello' })}>Send</button>
|
|
70
73
|
</div>
|
|
71
74
|
)
|
|
72
75
|
}
|
|
73
76
|
```
|
|
74
77
|
|
|
78
|
+
### consumeAgentStream (non-React, async iterable)
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { consumeAgentStream } from 'theokit/client'
|
|
82
|
+
|
|
83
|
+
const stream = consumeAgentStream('/api/agents/assistant', {
|
|
84
|
+
body: { message: 'Hello' },
|
|
85
|
+
})
|
|
86
|
+
for await (const event of stream) {
|
|
87
|
+
console.log(event.type, event.content)
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### parseSSEChunk (low-level SSE parser)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { parseSSEChunk } from 'theokit/client'
|
|
95
|
+
|
|
96
|
+
// Parse a single SSE line into an AgentEvent (or null)
|
|
97
|
+
const event = parseSSEChunk('data: {"type":"message","content":"Hello"}')
|
|
98
|
+
```
|
|
99
|
+
|
|
75
100
|
## Path Aliases
|
|
76
101
|
|
|
77
102
|
```typescript
|
|
@@ -83,7 +108,7 @@ Configured in `tsconfig.json` — works in both server and app code.
|
|
|
83
108
|
|
|
84
109
|
## Anti-patterns
|
|
85
110
|
|
|
86
|
-
- NEVER use `fetch('/api/...')`
|
|
111
|
+
- NEVER use raw `fetch('/api/...')` — use `theoFetch` for type safety
|
|
87
112
|
- NEVER create pages outside `app/` — they won't be discovered by the router
|
|
88
113
|
- NEVER import server code directly in `app/` — use theoFetch or server actions
|
|
89
114
|
- NEVER use `useEffect` + `fetch` for data loading — use theoFetch or useAgentStream
|
|
@@ -8,10 +8,8 @@
|
|
|
8
8
|
"build": "theokit build",
|
|
9
9
|
"start": "theokit start",
|
|
10
10
|
"test": "vitest run",
|
|
11
|
-
"seed": "npx tsx server/db/seed.ts",
|
|
12
11
|
"db:migrate": "theokit db migrate",
|
|
13
12
|
"db:generate": "theokit db generate",
|
|
14
|
-
"db:seed": "npx tsx server/db/seed.ts",
|
|
15
13
|
"lint": "eslint .",
|
|
16
14
|
"lint:fix": "eslint . --fix",
|
|
17
15
|
"format": "prettier --write .",
|
|
@@ -8,14 +8,3 @@ const sqlite = new Database('data/dev.db')
|
|
|
8
8
|
sqlite.pragma('journal_mode = WAL')
|
|
9
9
|
|
|
10
10
|
export const db = drizzle(sqlite, { schema })
|
|
11
|
-
|
|
12
|
-
// Auto-create tables (simple push — no migration files needed for dev)
|
|
13
|
-
sqlite.exec(`
|
|
14
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
15
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
-
title TEXT NOT NULL,
|
|
17
|
-
priority TEXT NOT NULL DEFAULT 'medium',
|
|
18
|
-
done INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
created_at TEXT NOT NULL DEFAULT (date('now'))
|
|
20
|
-
)
|
|
21
|
-
`)
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
// Define your Drizzle tables here. Example:
|
|
2
|
+
//
|
|
3
|
+
// import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
4
|
+
//
|
|
5
|
+
// export const posts = sqliteTable('posts', {
|
|
6
|
+
// id: integer('id').primaryKey({ autoIncrement: true }),
|
|
7
|
+
// title: text('title').notNull(),
|
|
8
|
+
// published: integer('published', { mode: 'boolean' }).notNull().default(false),
|
|
9
|
+
// createdAt: text('created_at').notNull().$defaultFn(() => new Date().toISOString()),
|
|
10
|
+
// })
|
|
11
|
+
//
|
|
12
|
+
// Or scaffold a full resource (schema + routes + test):
|
|
13
|
+
// npx theokit generate resource posts title:string published:boolean
|
|
@@ -1,38 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
|
-
import { z } from 'zod'
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
it('accepts valid task', () => {
|
|
13
|
-
const result = taskBodySchema.safeParse({ title: 'Buy groceries' })
|
|
14
|
-
expect(result.success).toBe(true)
|
|
15
|
-
if (result.success) {
|
|
16
|
-
expect(result.data.title).toBe('Buy groceries')
|
|
17
|
-
expect(result.data.done).toBe(false)
|
|
3
|
+
describe('Health route response shape', () => {
|
|
4
|
+
it('returns expected fields', () => {
|
|
5
|
+
// Mirrors the shape returned by server/routes/health.ts GET handler.
|
|
6
|
+
// Unit test — no server dependency.
|
|
7
|
+
const response = {
|
|
8
|
+
status: 'ok',
|
|
9
|
+
timestamp: Date.now(),
|
|
10
|
+
framework: 'TheoKit',
|
|
18
11
|
}
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
it('defaults done to false', () => {
|
|
22
|
-
const result = taskBodySchema.safeParse({ title: 'New task' })
|
|
23
|
-
expect(result.success).toBe(true)
|
|
24
|
-
if (result.success) expect(result.data.done).toBe(false)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('rejects title shorter than 3 characters', () => {
|
|
28
|
-
expect(taskBodySchema.safeParse({ title: 'ab' }).success).toBe(false)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('rejects empty title', () => {
|
|
32
|
-
expect(taskBodySchema.safeParse({ title: '' }).success).toBe(false)
|
|
33
|
-
})
|
|
34
12
|
|
|
35
|
-
|
|
36
|
-
expect(
|
|
13
|
+
expect(response.status).toBe('ok')
|
|
14
|
+
expect(response.framework).toBe('TheoKit')
|
|
15
|
+
expect(typeof response.timestamp).toBe('number')
|
|
16
|
+
expect(response.timestamp).toBeGreaterThan(0)
|
|
37
17
|
})
|
|
38
18
|
})
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AssistantAgent — AI-powered task management assistant.
|
|
3
|
-
*
|
|
4
|
-
* Uses the SAME guards and pipeline as HTTP controllers.
|
|
5
|
-
* Tools from @Mixin(TaskTools) are available to the LLM.
|
|
6
|
-
*/
|
|
7
|
-
import 'reflect-metadata'
|
|
8
|
-
import {
|
|
9
|
-
Agent, MainLoop, Mixin,
|
|
10
|
-
Memory, Budget, Hook,
|
|
11
|
-
} from '@theokit/agents'
|
|
12
|
-
import { UseGuards, UseInterceptors } from '@theokit/http'
|
|
13
|
-
import { RolesGuard, Roles, Role } from '../guards/auth.guard.js'
|
|
14
|
-
import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
|
|
15
|
-
import { TaskTools } from '../toolboxes/task.tools.js'
|
|
16
|
-
|
|
17
|
-
@Agent({
|
|
18
|
-
// Convention: AssistantAgent → name: 'assistant', route: /api/agents/assistant
|
|
19
|
-
model: 'openai/gpt-4o-mini',
|
|
20
|
-
systemPrompt: `You are a helpful task management assistant.
|
|
21
|
-
Use the tasks.* tools to list, search, create, and complete tasks.
|
|
22
|
-
Be concise and actionable.`,
|
|
23
|
-
})
|
|
24
|
-
@UseGuards(RolesGuard)
|
|
25
|
-
@UseInterceptors(TimingInterceptor)
|
|
26
|
-
@Roles([Role.User])
|
|
27
|
-
@Memory({ provider: 'built-in', scope: 'per-user' })
|
|
28
|
-
@Budget({ maxCostUsd: 1.00, window: 'daily' })
|
|
29
|
-
@Mixin(TaskTools)
|
|
30
|
-
export class AssistantAgent {
|
|
31
|
-
@MainLoop({ strategy: 'react', maxIterations: 5 })
|
|
32
|
-
async run() {}
|
|
33
|
-
|
|
34
|
-
@Hook('before:llm-call')
|
|
35
|
-
async onBeforeLLM() {
|
|
36
|
-
console.log(' 🧠 Agent thinking...')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@Hook('after:tool-call')
|
|
40
|
-
async onToolDone() {
|
|
41
|
-
console.log(' 🔧 Tool executed')
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TasksController — CRUD API for tasks.
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates: @Controller, @Get/@Post/@Delete, @Body with Zod,
|
|
5
|
-
* @Param, @Query, @HttpCode, @UseGuards, @Roles, @IsPublic,
|
|
6
|
-
* @UseInterceptors, @UseFilters, NotFoundException.
|
|
7
|
-
*/
|
|
8
|
-
import 'reflect-metadata'
|
|
9
|
-
import { z } from 'zod'
|
|
10
|
-
import {
|
|
11
|
-
Controller, Get, Post, Put, Delete,
|
|
12
|
-
Body, Param, Query, HttpCode,
|
|
13
|
-
UseGuards, UseInterceptors, UseFilters,
|
|
14
|
-
NotFoundException,
|
|
15
|
-
} from '@theokit/http'
|
|
16
|
-
import { RolesGuard, Roles, Role, IsPublic } from '../guards/auth.guard.js'
|
|
17
|
-
import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
|
|
18
|
-
import { HttpErrorFilter } from '../filters/http-error.filter.js'
|
|
19
|
-
import { taskStore } from '../store.js'
|
|
20
|
-
|
|
21
|
-
const zCreateTask = z.object({
|
|
22
|
-
title: z.string().min(3, 'Title must be at least 3 characters'),
|
|
23
|
-
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
@Controller() // → /api/tasks (convention: inferred from TasksController)
|
|
27
|
-
@UseGuards(RolesGuard)
|
|
28
|
-
@UseInterceptors(TimingInterceptor)
|
|
29
|
-
@UseFilters(HttpErrorFilter)
|
|
30
|
-
@Roles([Role.User])
|
|
31
|
-
export class TasksController {
|
|
32
|
-
@Get()
|
|
33
|
-
@IsPublic(true)
|
|
34
|
-
list() {
|
|
35
|
-
return taskStore.list()
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
@Get('search')
|
|
39
|
-
@IsPublic(true)
|
|
40
|
-
search(@Query('q') q: string) {
|
|
41
|
-
return taskStore.search(q ?? '')
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
@Get(':id')
|
|
45
|
-
@IsPublic(true)
|
|
46
|
-
findById(@Param('id') id: string) {
|
|
47
|
-
const task = taskStore.get(Number(id))
|
|
48
|
-
if (!task) throw new NotFoundException(`Task ${id} not found`)
|
|
49
|
-
return task
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
@Post()
|
|
53
|
-
create(@Body(zCreateTask) body: z.infer<typeof zCreateTask>) {
|
|
54
|
-
return taskStore.create(body)
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
@Put(':id')
|
|
58
|
-
update(@Param('id') id: string, @Body(z.object({ done: z.boolean() })) body: { done: boolean }) {
|
|
59
|
-
const task = taskStore.update(Number(id), body)
|
|
60
|
-
if (!task) throw new NotFoundException(`Task ${id} not found`)
|
|
61
|
-
return task
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@Delete(':id')
|
|
65
|
-
@HttpCode(204)
|
|
66
|
-
@Roles([Role.Admin])
|
|
67
|
-
remove(@Param('id') id: string) {
|
|
68
|
-
if (!taskStore.remove(Number(id))) throw new NotFoundException(`Task ${id} not found`)
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { db } from './index.js'
|
|
2
|
-
import { tasks } from './schema.js'
|
|
3
|
-
import { sql } from 'drizzle-orm'
|
|
4
|
-
|
|
5
|
-
const seedData = [
|
|
6
|
-
{
|
|
7
|
-
title: 'Set up TheoKit project',
|
|
8
|
-
priority: 'high' as const,
|
|
9
|
-
done: true,
|
|
10
|
-
createdAt: '2026-01-01',
|
|
11
|
-
},
|
|
12
|
-
{ title: 'Create first route', priority: 'high' as const, done: true, createdAt: '2026-01-02' },
|
|
13
|
-
{ title: 'Add AI agent', priority: 'medium' as const, done: false, createdAt: '2026-01-03' },
|
|
14
|
-
{ title: 'Deploy to production', priority: 'low' as const, done: false, createdAt: '2026-01-04' },
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
// Idempotent: only insert if table is empty
|
|
18
|
-
const count = db
|
|
19
|
-
.select({ count: sql<number>`count(*)` })
|
|
20
|
-
.from(tasks)
|
|
21
|
-
.get()
|
|
22
|
-
if (!count || count.count === 0) {
|
|
23
|
-
for (const task of seedData) {
|
|
24
|
-
db.insert(tasks).values(task).run()
|
|
25
|
-
}
|
|
26
|
-
console.log(`Seeded ${seedData.length} tasks`)
|
|
27
|
-
} else {
|
|
28
|
-
console.log(`Tasks table already has ${count.count} rows — skipping seed`)
|
|
29
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HttpErrorFilter — custom error response format.
|
|
3
|
-
* Catches HttpException and returns structured JSON.
|
|
4
|
-
*/
|
|
5
|
-
import { Catch, HttpException, type ExceptionFilter, type ArgumentsHost } from '@theokit/http'
|
|
6
|
-
|
|
7
|
-
@Catch(HttpException)
|
|
8
|
-
export class HttpErrorFilter implements ExceptionFilter {
|
|
9
|
-
catch(exception: unknown, _host: ArgumentsHost): Response {
|
|
10
|
-
const ex = exception as HttpException
|
|
11
|
-
return new Response(JSON.stringify({
|
|
12
|
-
success: false,
|
|
13
|
-
error: { code: ex.statusCode, message: ex.message },
|
|
14
|
-
timestamp: new Date().toISOString(),
|
|
15
|
-
}), {
|
|
16
|
-
status: ex.statusCode,
|
|
17
|
-
headers: { 'content-type': 'application/json' },
|
|
18
|
-
})
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RolesGuard — RBAC authorization for controllers AND agents.
|
|
3
|
-
*
|
|
4
|
-
* Same guard, same pipeline, same behavior on both HTTP and AI routes.
|
|
5
|
-
* Uses @Roles([Role.Admin]) on class/method and @IsPublic(true) to skip.
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
createDecorator, Reflector,
|
|
9
|
-
type CanActivate, type ExecutionContext,
|
|
10
|
-
} from '@theokit/http'
|
|
11
|
-
|
|
12
|
-
export enum Role {
|
|
13
|
-
User = 'user',
|
|
14
|
-
Admin = 'admin',
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Declare required roles for a route or agent. */
|
|
18
|
-
export const Roles = createDecorator<Role[]>()
|
|
19
|
-
|
|
20
|
-
/** Mark a route as public — skips auth entirely. */
|
|
21
|
-
export const IsPublic = createDecorator<boolean>()
|
|
22
|
-
|
|
23
|
-
const reflector = new Reflector()
|
|
24
|
-
|
|
25
|
-
export class RolesGuard implements CanActivate {
|
|
26
|
-
canActivate(context: ExecutionContext): boolean {
|
|
27
|
-
// Public routes skip auth
|
|
28
|
-
const isPublic = reflector.getAllAndOverride(
|
|
29
|
-
IsPublic,
|
|
30
|
-
context.getClass(),
|
|
31
|
-
context.getMethodName(),
|
|
32
|
-
)
|
|
33
|
-
if (isPublic) return true
|
|
34
|
-
|
|
35
|
-
// Check required roles
|
|
36
|
-
const roles = reflector.getAllAndOverride(
|
|
37
|
-
Roles,
|
|
38
|
-
context.getClass(),
|
|
39
|
-
context.getMethodName(),
|
|
40
|
-
)
|
|
41
|
-
if (!roles) return true // no roles required
|
|
42
|
-
|
|
43
|
-
// Read role from request header (replace with JWT/session in production)
|
|
44
|
-
const userRole = context.getRequest().headers.get('x-role') ?? ''
|
|
45
|
-
return roles.some((r) => r === userRole)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TimingInterceptor — logs request duration.
|
|
3
|
-
* Applied to both controllers and agents.
|
|
4
|
-
*/
|
|
5
|
-
import type { Interceptor } from '@theokit/http'
|
|
6
|
-
|
|
7
|
-
export class TimingInterceptor implements Interceptor {
|
|
8
|
-
async intercept(_request: Request, next: () => Promise<unknown>): Promise<unknown> {
|
|
9
|
-
const start = Date.now()
|
|
10
|
-
const result = await next()
|
|
11
|
-
console.log(` ${Date.now() - start}ms`)
|
|
12
|
-
return result
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LoggerMiddleware — logs every incoming request.
|
|
3
|
-
*/
|
|
4
|
-
import type { NestMiddleware } from '@theokit/http'
|
|
5
|
-
|
|
6
|
-
export class LoggerMiddleware implements NestMiddleware {
|
|
7
|
-
use(request: Request, next: () => Promise<Response | null>): Promise<Response | null> {
|
|
8
|
-
const url = new URL(request.url)
|
|
9
|
-
console.log(` ${new Date().toLocaleTimeString()} ${request.method} ${url.pathname}`)
|
|
10
|
-
return next()
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { defineRoute } from 'theokit/server/define'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { db } from '../../db/index.js'
|
|
4
|
-
import { tasks } from '../../db/schema.js'
|
|
5
|
-
import { eq } from 'drizzle-orm'
|
|
6
|
-
|
|
7
|
-
export const GET = defineRoute({
|
|
8
|
-
params: z.object({ id: z.coerce.number() }),
|
|
9
|
-
handler: ({ params }) => {
|
|
10
|
-
const task = db.select().from(tasks).where(eq(tasks.id, params.id)).get()
|
|
11
|
-
if (!task) return new Response(JSON.stringify({ error: 'Task not found' }), { status: 404 })
|
|
12
|
-
return task
|
|
13
|
-
},
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
export const PUT = defineRoute({
|
|
17
|
-
params: z.object({ id: z.coerce.number() }),
|
|
18
|
-
body: z.object({
|
|
19
|
-
title: z.string().optional(),
|
|
20
|
-
priority: z.enum(['low', 'medium', 'high']).optional(),
|
|
21
|
-
done: z.boolean().optional(),
|
|
22
|
-
}),
|
|
23
|
-
handler: ({ params, body }) => {
|
|
24
|
-
const result = db.update(tasks).set(body).where(eq(tasks.id, params.id)).returning().get()
|
|
25
|
-
if (!result) return new Response(JSON.stringify({ error: 'Task not found' }), { status: 404 })
|
|
26
|
-
return result
|
|
27
|
-
},
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
export const DELETE = defineRoute({
|
|
31
|
-
params: z.object({ id: z.coerce.number() }),
|
|
32
|
-
status: 204,
|
|
33
|
-
handler: ({ params }) => {
|
|
34
|
-
db.delete(tasks).where(eq(tasks.id, params.id)).run()
|
|
35
|
-
},
|
|
36
|
-
})
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { defineRoute } from 'theokit/server/define'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { db } from '../../db/index.js'
|
|
4
|
-
import { tasks } from '../../db/schema.js'
|
|
5
|
-
|
|
6
|
-
export const GET = defineRoute({
|
|
7
|
-
handler: () => db.select().from(tasks).all(),
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
export const POST = defineRoute({
|
|
11
|
-
body: z.object({
|
|
12
|
-
title: z.string().min(3, 'Title must be at least 3 characters'),
|
|
13
|
-
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
14
|
-
}),
|
|
15
|
-
status: 201,
|
|
16
|
-
handler: ({ body }) => {
|
|
17
|
-
const result = db.insert(tasks).values(body).returning().get()
|
|
18
|
-
return result
|
|
19
|
-
},
|
|
20
|
-
})
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-memory data store.
|
|
3
|
-
* Replace with Drizzle + PostgreSQL for production.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface Task {
|
|
7
|
-
id: number
|
|
8
|
-
title: string
|
|
9
|
-
priority: 'low' | 'medium' | 'high'
|
|
10
|
-
done: boolean
|
|
11
|
-
createdAt: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
let seq = 0
|
|
15
|
-
|
|
16
|
-
const tasks: Task[] = [
|
|
17
|
-
{ id: ++seq, title: 'Set up TheoKit project', priority: 'high', done: true, createdAt: '2026-01-01' },
|
|
18
|
-
{ id: ++seq, title: 'Create first controller', priority: 'high', done: true, createdAt: '2026-01-02' },
|
|
19
|
-
{ id: ++seq, title: 'Add AI agent', priority: 'medium', done: false, createdAt: '2026-01-03' },
|
|
20
|
-
{ id: ++seq, title: 'Deploy to production', priority: 'low', done: false, createdAt: '2026-01-04' },
|
|
21
|
-
]
|
|
22
|
-
|
|
23
|
-
export const taskStore = {
|
|
24
|
-
list: () => [...tasks],
|
|
25
|
-
get: (id: number) => tasks.find((t) => t.id === id),
|
|
26
|
-
search: (q: string) => tasks.filter((t) => t.title.toLowerCase().includes(q.toLowerCase())),
|
|
27
|
-
create: (data: { title: string; priority?: Task['priority'] }) => {
|
|
28
|
-
const task: Task = { id: ++seq, title: data.title, priority: data.priority ?? 'medium', done: false, createdAt: new Date().toISOString().split('T')[0] }
|
|
29
|
-
tasks.push(task)
|
|
30
|
-
return task
|
|
31
|
-
},
|
|
32
|
-
update: (id: number, data: Partial<Pick<Task, 'title' | 'priority' | 'done'>>) => {
|
|
33
|
-
const t = tasks.find((x) => x.id === id)
|
|
34
|
-
if (!t) return null
|
|
35
|
-
if (data.title !== undefined) t.title = data.title
|
|
36
|
-
if (data.priority !== undefined) t.priority = data.priority
|
|
37
|
-
if (data.done !== undefined) t.done = data.done
|
|
38
|
-
return t
|
|
39
|
-
},
|
|
40
|
-
remove: (id: number) => {
|
|
41
|
-
const idx = tasks.findIndex((t) => t.id === id)
|
|
42
|
-
if (idx === -1) return false
|
|
43
|
-
tasks.splice(idx, 1)
|
|
44
|
-
return true
|
|
45
|
-
},
|
|
46
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TaskTools — agent toolbox for task management.
|
|
3
|
-
*
|
|
4
|
-
* These tools are called by the LLM when the agent decides
|
|
5
|
-
* to interact with the task data. Each @Tool method is
|
|
6
|
-
* compiled to a defineTool() call at startup.
|
|
7
|
-
*/
|
|
8
|
-
import 'reflect-metadata'
|
|
9
|
-
import { z } from 'zod'
|
|
10
|
-
import { Toolbox, Tool, Trace, Audit } from '@theokit/agents'
|
|
11
|
-
import { taskStore } from '../store.js'
|
|
12
|
-
|
|
13
|
-
@Toolbox() // Convention: TaskTools → namespace: 'task'
|
|
14
|
-
@Trace(true)
|
|
15
|
-
export class TaskTools {
|
|
16
|
-
@Tool({
|
|
17
|
-
name: 'list',
|
|
18
|
-
description: 'List all tasks with their status and priority',
|
|
19
|
-
input: z.object({}),
|
|
20
|
-
})
|
|
21
|
-
async list() {
|
|
22
|
-
return JSON.stringify(taskStore.list())
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Tool({
|
|
26
|
-
name: 'search',
|
|
27
|
-
description: 'Search tasks by keyword in title',
|
|
28
|
-
input: z.object({ query: z.string() }),
|
|
29
|
-
})
|
|
30
|
-
async search(input: { query: string }) {
|
|
31
|
-
return JSON.stringify(taskStore.search(input.query))
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
@Tool({
|
|
35
|
-
name: 'create',
|
|
36
|
-
description: 'Create a new task',
|
|
37
|
-
input: z.object({
|
|
38
|
-
title: z.string(),
|
|
39
|
-
priority: z.enum(['low', 'medium', 'high']).default('medium'),
|
|
40
|
-
}),
|
|
41
|
-
risk: 'medium',
|
|
42
|
-
})
|
|
43
|
-
@Audit(true)
|
|
44
|
-
async create(input: { title: string; priority?: 'low' | 'medium' | 'high' }) {
|
|
45
|
-
return JSON.stringify(taskStore.create(input))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
@Tool({
|
|
49
|
-
name: 'complete',
|
|
50
|
-
description: 'Mark a task as done by its ID',
|
|
51
|
-
input: z.object({ taskId: z.number() }),
|
|
52
|
-
})
|
|
53
|
-
@Audit(true)
|
|
54
|
-
async complete(input: { taskId: number }) {
|
|
55
|
-
const task = taskStore.update(input.taskId, { done: true })
|
|
56
|
-
return task ? JSON.stringify(task) : 'Task not found'
|
|
57
|
-
}
|
|
58
|
-
}
|