create-theokit 1.0.8 → 1.0.10
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/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 +14 -6
- 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)
|
|
@@ -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,10 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest'
|
|
2
2
|
|
|
3
|
-
describe('
|
|
4
|
-
it('
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
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',
|
|
11
|
+
}
|
|
12
|
+
|
|
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)
|
|
9
17
|
})
|
|
10
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
|
-
}
|