create-theokit 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/cli.js +5 -0
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/default/AGENTS.md +41 -51
  5. package/templates/default/CLAUDE.md +25 -0
  6. package/templates/default/README.md.tmpl +53 -51
  7. package/templates/default/_gitignore +31 -0
  8. package/templates/default/app/page.tsx +78 -39
  9. package/templates/default/app.ts +24 -0
  10. package/templates/default/dot-claude/rules/theokit-conventions.md +36 -0
  11. package/templates/default/dot-claude/settings.json +23 -0
  12. package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +105 -0
  13. package/templates/default/dot-claude/skills/theokit-config/SKILL.md +128 -0
  14. package/templates/default/dot-claude/skills/theokit-database/SKILL.md +112 -0
  15. package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +89 -0
  16. package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +89 -0
  17. package/templates/default/drizzle.config.ts +10 -0
  18. package/templates/default/eslint.config.mjs +5 -1
  19. package/templates/default/package.json.tmpl +13 -3
  20. package/templates/default/public/index.html +70 -0
  21. package/templates/default/server/agents/assistant.agent.ts +6 -2
  22. package/templates/default/server/controllers/tasks.controller.ts +5 -13
  23. package/templates/default/server/db/index.ts +21 -0
  24. package/templates/default/server/db/schema.ts +13 -0
  25. package/templates/default/server/db/seed.ts +29 -0
  26. package/templates/default/server/index.ts +2 -18
  27. package/templates/default/server/routes/tasks/[id].ts +36 -0
  28. package/templates/default/server/routes/tasks/index.ts +20 -0
  29. package/templates/default/server/toolboxes/task.tools.ts +2 -1
  30. package/templates/default/tests/tasks.test.ts +38 -0
  31. package/templates/default/theo.config.ts +0 -6
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: theokit-frontend
3
+ description: TheoKit frontend — file-based routing, layouts, theoFetch typed client, useAgentStream, React patterns
4
+ user-invocable: false
5
+ paths:
6
+ - "app/**"
7
+ ---
8
+
9
+ # TheoKit Frontend (React + File-Based Routing)
10
+
11
+ ## File-Based Routing
12
+
13
+ | File | URL | Purpose |
14
+ |------|-----|---------|
15
+ | `app/page.tsx` | `/` | Home page |
16
+ | `app/layout.tsx` | (wrapper) | Root layout (wraps all pages) |
17
+ | `app/error.tsx` | (error) | Error boundary |
18
+ | `app/loading.tsx` | (loading) | Suspense fallback |
19
+ | `app/not-found.tsx` | (404) | Not found page |
20
+ | `app/about/page.tsx` | `/about` | Nested route |
21
+ | `app/tasks/[id]/page.tsx` | `/tasks/:id` | Dynamic route |
22
+
23
+ ## Typed API Client (theoFetch)
24
+
25
+ ```typescript
26
+ import { theoFetch } from 'theokit/client'
27
+
28
+ // Typed fetch — params and response inferred from server routes
29
+ const tasks = await theoFetch('/api/tasks')
30
+ const task = await theoFetch('/api/tasks/:id', { params: { id: 1 } })
31
+
32
+ // POST with body
33
+ const created = await theoFetch('/api/tasks', {
34
+ method: 'POST',
35
+ body: { title: 'New task' },
36
+ })
37
+ ```
38
+
39
+ ## App Client (proxy-based)
40
+
41
+ ```typescript
42
+ import { createAppClient } from 'theokit/client'
43
+
44
+ const client = createAppClient()
45
+
46
+ // Proxy pattern — method names match route structure
47
+ const tasks = await client.tasks.GET()
48
+ const task = await client.tasks[':id'].GET({ params: { id: 1 } })
49
+ const created = await client.tasks.POST({ body: { title: 'New' } })
50
+ ```
51
+
52
+ ## Agent Streaming (useAgentStream)
53
+
54
+ ```typescript
55
+ import { useAgentStream } from 'theokit/client'
56
+
57
+ function ChatComponent() {
58
+ const { status, events, send, reset } = useAgentStream('/api/agents/assistant')
59
+
60
+ return (
61
+ <div>
62
+ {status === 'streaming' && <p>Thinking...</p>}
63
+ {events.map(event => (
64
+ <div key={event.id}>
65
+ {event.type === 'message' && <p>{event.content}</p>}
66
+ {event.type === 'tool_call' && <p>Using tool: {event.name}</p>}
67
+ </div>
68
+ ))}
69
+ <input onSubmit={e => send({ message: e.target.value })} />
70
+ </div>
71
+ )
72
+ }
73
+ ```
74
+
75
+ ## Path Aliases
76
+
77
+ ```typescript
78
+ import { db } from '@/server/db' // @/ = project root
79
+ import { tasks } from '@/server/db/schema'
80
+ ```
81
+
82
+ Configured in `tsconfig.json` — works in both server and app code.
83
+
84
+ ## Anti-patterns
85
+
86
+ - NEVER use `fetch('/api/...')` directly — use `theoFetch` for type safety
87
+ - NEVER create pages outside `app/` — they won't be discovered by the router
88
+ - NEVER import server code directly in `app/` — use theoFetch or server actions
89
+ - NEVER use `useEffect` + `fetch` for data loading — use theoFetch or useAgentStream
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: theokit-routes
3
+ description: TheoKit server routes — defineRoute, Zod validation, HTTP methods, dynamic params, error handling
4
+ user-invocable: false
5
+ paths:
6
+ - "server/routes/**"
7
+ - "server/actions/**"
8
+ ---
9
+
10
+ # TheoKit Routes
11
+
12
+ ## defineRoute API
13
+
14
+ ```typescript
15
+ import { defineRoute } from 'theokit/server/define'
16
+ import { z } from 'zod'
17
+
18
+ // GET handler — no body, optional params/query
19
+ export const GET = defineRoute({
20
+ params: z.object({ id: z.coerce.number() }), // URL params
21
+ query: z.object({ page: z.coerce.number().optional() }), // Query string
22
+ handler: ({ params, query }) => {
23
+ return { id: params.id, page: query?.page }
24
+ },
25
+ })
26
+
27
+ // POST handler — with body validation + custom status
28
+ export const POST = defineRoute({
29
+ body: z.object({
30
+ title: z.string().min(3),
31
+ done: z.boolean().default(false),
32
+ }),
33
+ status: 201,
34
+ handler: ({ body }) => {
35
+ // body is fully typed from Zod schema
36
+ return db.insert(tasks).values(body).returning().get()
37
+ },
38
+ })
39
+
40
+ // PUT, DELETE follow the same pattern
41
+ export const PUT = defineRoute({ body: z.object({...}), handler: ({body, params}) => {...} })
42
+ export const DELETE = defineRoute({ params: z.object({id: z.coerce.number()}), handler: ({params}) => {...} })
43
+ ```
44
+
45
+ ## File-to-URL Mapping
46
+
47
+ | File path | URL | Notes |
48
+ |-----------|-----|-------|
49
+ | `server/routes/health.ts` | `GET /api/health` | Static route |
50
+ | `server/routes/tasks/index.ts` | `/api/tasks` | Index route (GET + POST) |
51
+ | `server/routes/tasks/[id].ts` | `/api/tasks/:id` | Dynamic param |
52
+ | `server/routes/users/[...slug].ts` | `/api/users/*` | Catch-all |
53
+
54
+ ## defineAction (Server Actions)
55
+
56
+ ```typescript
57
+ import { defineAction } from 'theokit/server/define'
58
+ import { z } from 'zod'
59
+
60
+ export const createTask = defineAction({
61
+ input: z.object({ title: z.string() }),
62
+ handler: ({ input }) => {
63
+ return db.insert(tasks).values(input).returning().get()
64
+ },
65
+ })
66
+ ```
67
+
68
+ ## Error Handling
69
+
70
+ ```typescript
71
+ import { TheoError } from 'theokit'
72
+
73
+ export const GET = defineRoute({
74
+ handler: ({ params }) => {
75
+ const task = db.select().from(tasks).where(eq(tasks.id, params.id)).get()
76
+ if (!task) throw new TheoError({ code: 'NOT_FOUND', message: 'Task not found' })
77
+ return task
78
+ },
79
+ })
80
+ ```
81
+
82
+ Valid error codes: `BAD_REQUEST` (400), `UNAUTHORIZED` (401), `FORBIDDEN` (403), `NOT_FOUND` (404), `CONFLICT` (409), `UNPROCESSABLE_ENTITY` (422), `TOO_MANY_REQUESTS` (429), `INTERNAL_SERVER_ERROR` (500).
83
+
84
+ ## Anti-patterns
85
+
86
+ - NEVER use `res.status().json()` — use defineRoute with `status:` option
87
+ - NEVER parse `req.body` manually — use `body: z.object(...)` in defineRoute
88
+ - NEVER create routes outside `server/routes/` — they won't be discovered
89
+ - NEVER export non-HTTP-method names — only `GET`, `POST`, `PUT`, `DELETE`, `PATCH`
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'drizzle-kit'
2
+
3
+ export default defineConfig({
4
+ schema: './server/db/schema.ts',
5
+ out: './drizzle',
6
+ dialect: 'sqlite',
7
+ dbCredentials: {
8
+ url: './data/dev.db',
9
+ },
10
+ })
@@ -1,14 +1,18 @@
1
1
  import tseslint from 'typescript-eslint'
2
2
  import prettierConfig from 'eslint-config-prettier'
3
+ import drizzle from 'eslint-plugin-drizzle'
3
4
 
4
5
  export default tseslint.config(
5
- { ignores: ['dist/', 'node_modules/'] },
6
+ { ignores: ['dist/', 'node_modules/', 'drizzle/'] },
6
7
  ...tseslint.configs.recommended,
7
8
  prettierConfig,
8
9
  {
10
+ plugins: { drizzle },
9
11
  rules: {
10
12
  '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
11
13
  '@typescript-eslint/no-explicit-any': 'warn',
14
+ 'drizzle/enforce-delete-with-where': 'error',
15
+ 'drizzle/enforce-update-with-where': 'error',
12
16
  },
13
17
  },
14
18
  )
@@ -7,6 +7,11 @@
7
7
  "dev": "theokit dev",
8
8
  "build": "theokit build",
9
9
  "start": "theokit start",
10
+ "test": "vitest run",
11
+ "seed": "npx tsx server/db/seed.ts",
12
+ "db:migrate": "theokit db migrate",
13
+ "db:generate": "theokit db generate",
14
+ "db:seed": "npx tsx server/db/seed.ts",
10
15
  "lint": "eslint .",
11
16
  "lint:fix": "eslint . --fix",
12
17
  "format": "prettier --write .",
@@ -18,16 +23,21 @@
18
23
  "react": "^19.0.0",
19
24
  "react-dom": "^19.0.0",
20
25
  "react-router": "^7.0.0",
21
- "zod": "^4.0.0"
26
+ "zod": "^4.0.0",
27
+ "drizzle-orm": "^0.44.0",
28
+ "better-sqlite3": "^12.0.0"
22
29
  },
23
30
  "devDependencies": {
24
- "@swc/core": "^1.3.0",
31
+ "@types/better-sqlite3": "^7.6.0",
25
32
  "@types/react": "^19.0.0",
26
33
  "@types/react-dom": "^19.0.0",
34
+ "drizzle-kit": "^0.30.0",
27
35
  "eslint": "^9.0.0",
28
36
  "eslint-config-prettier": "^10.0.0",
37
+ "eslint-plugin-drizzle": "^0.2.0",
29
38
  "prettier": "^3.0.0",
30
39
  "typescript": "^5.5.0",
31
- "typescript-eslint": "^8.0.0"
40
+ "typescript-eslint": "^8.0.0",
41
+ "vitest": "^3.0.0"
32
42
  }
33
43
  }
@@ -0,0 +1,70 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
5
+ <title>TheoKit App</title>
6
+ <style>
7
+ :root{--bg:#0a0a0a;--card:#141414;--border:#2a2a2a;--text:#e0e0e0;--muted:#888;--accent:#6366f1;--green:#22c55e;--red:#ef4444;--yellow:#eab308}
8
+ *{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
9
+ #app{max-width:1200px;margin:0 auto;padding:20px}h1{font-size:1.6rem}.accent{color:var(--accent)}.subtitle{color:var(--muted);font-size:.85rem;margin-top:2px}
10
+ .role-bar{margin:12px 0}.role-bar select{padding:6px 12px;background:#1a1a1a;border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.8rem}
11
+ .grid{display:grid;grid-template-columns:1fr 1fr;gap:20px}@media(max-width:768px){.grid{grid-template-columns:1fr}}
12
+ .card{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:20px}
13
+ h2{font-size:1rem;margin-bottom:14px;display:flex;align-items:center;gap:8px}.badge{font-size:.65rem;padding:2px 8px;border-radius:99px;background:#6366f122;color:var(--accent)}.badge-ai{background:#eab30822;color:var(--yellow)}
14
+ table{width:100%;border-collapse:collapse;font-size:.85rem}th{text-align:left;padding:8px 4px;color:var(--muted);border-bottom:1px solid var(--border);font-weight:500}td{padding:8px 4px;border-bottom:1px solid var(--border)}
15
+ tr.done td{opacity:.5;text-decoration:line-through}.prio{font-size:.7rem;padding:2px 8px;border-radius:99px}.prio-high{background:#ef444422;color:var(--red)}.prio-med{background:#eab30822;color:var(--yellow)}.prio-low{background:#22c55e22;color:var(--green)}
16
+ .create-bar{display:flex;gap:8px;margin-top:14px}.create-bar input{flex:1;padding:8px 12px;background:#1a1a1a;border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.85rem}
17
+ .create-bar select{padding:8px;background:#1a1a1a;border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:.8rem}.create-bar button{padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600}
18
+ .error{color:var(--red);font-size:.8rem;margin-top:4px}
19
+ .chat-box{height:400px;overflow-y:auto;padding:12px;background:#0d0d0d;border-radius:8px;margin-bottom:10px;font-size:.85rem;line-height:1.6}
20
+ .msg{margin-bottom:10px;padding:8px 12px;border-radius:8px}.msg.user{background:#6366f118;color:var(--accent)}.msg.agent{background:#1a1a1a}.msg.tool{background:#eab30810;color:var(--yellow);font-size:.78rem;font-family:monospace}.msg.system{color:var(--muted);font-size:.78rem;font-style:italic}.msg.error{color:var(--red)}
21
+ .chat-bar{display:flex;gap:8px}.chat-bar input{flex:1;padding:10px 14px;background:#1a1a1a;border:1px solid var(--border);border-radius:8px;color:var(--text);font-size:.9rem;outline:none}.chat-bar input:focus{border-color:var(--accent)}
22
+ .chat-bar button{padding:10px 20px;background:var(--accent);color:#fff;border:none;border-radius:8px;cursor:pointer;font-weight:600}.chat-bar button:disabled{opacity:.4;cursor:not-allowed}
23
+ .cost{color:var(--muted);font-size:.75rem;margin-top:6px;text-align:right}
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <div id="app">
28
+ <header>
29
+ <h1><span class="accent">TheoKit</span> App</h1>
30
+ <p class="subtitle">Controllers + AI Agent — same pipeline</p>
31
+ <div class="role-bar"><label>Role: </label><select id="role"><option value="">None</option><option value="user" selected>User</option><option value="admin">Admin</option></select></div>
32
+ </header>
33
+ <main class="grid">
34
+ <section class="card">
35
+ <h2>📋 Tasks <span class="badge">@Controller</span></h2>
36
+ <table><thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead><tbody id="task-list"></tbody></table>
37
+ <form id="create-form" class="create-bar"><input id="new-title" placeholder="New task..." required minlength="3"><select id="new-priority"><option value="medium">Medium</option><option value="high">High</option><option value="low">Low</option></select><button type="submit">Add</button></form>
38
+ <p id="form-error" class="error"></p>
39
+ </section>
40
+ <section class="card">
41
+ <h2>🤖 AI Assistant <span class="badge badge-ai">@Agent + SSE</span></h2>
42
+ <div id="chat" class="chat-box"><div class="msg system">Ask me to list, create, or complete tasks...</div></div>
43
+ <div class="chat-bar"><input id="chat-input" placeholder="Message the AI assistant..."><button id="chat-send">Send</button></div>
44
+ <p id="chat-cost" class="cost"></p>
45
+ </section>
46
+ </main>
47
+ </div>
48
+ <script>
49
+ const getRole=()=>document.getElementById('role').value
50
+ const headers=()=>{const h={'Content-Type':'application/json'};const r=getRole();if(r)h['x-role']=r;return h}
51
+ let sessionId='s-'+Date.now()
52
+
53
+ async function loadTasks(){const r=await fetch('/api/tasks');const t=await r.json();document.getElementById('task-list').innerHTML=t.map(t=>{const s=t.done?'done':'';const p=t.priority==='high'?'prio-high':t.priority==='low'?'prio-low':'prio-med';return'<tr class="'+s+'"><td>'+(t.done?'✅ ':'○ ')+t.title+'</td><td><span class="prio '+p+'">'+t.priority+'</span></td><td>'+(t.done?'Done':'To do')+'</td></tr>'}).join('')}
54
+
55
+ document.getElementById('create-form').addEventListener('submit',async e=>{e.preventDefault();const t=document.getElementById('new-title').value.trim();const p=document.getElementById('new-priority').value;const err=document.getElementById('form-error');err.textContent='';if(!t)return;const r=await fetch('/api/tasks',{method:'POST',headers:headers(),body:JSON.stringify({title:t,priority:p})});if(r.status===403){err.textContent='403 — Need User role';return}if(r.status===422){const e=await r.json();err.textContent=e.error?.issues?.[0]?.message||'Validation error';return}if(!r.ok){err.textContent='Error '+r.status;return}document.getElementById('new-title').value='';loadTasks()})
56
+
57
+ document.getElementById('chat-send').addEventListener('click',sendChat)
58
+ document.getElementById('chat-input').addEventListener('keydown',e=>{if(e.key==='Enter')sendChat()})
59
+
60
+ async function sendChat(){const input=document.getElementById('chat-input');const msg=input.value.trim();if(!msg)return;input.value='';const chat=document.getElementById('chat');chat.innerHTML+='<div class="msg user">You: '+msg.replace(/</g,'&lt;')+'</div>';document.getElementById('chat-send').disabled=true
61
+ try{const r=await fetch('/api/agents/assistant/chat',{method:'POST',headers:headers(),body:JSON.stringify({message:msg,sessionId})});if(r.status===403){chat.innerHTML+='<div class="msg system">403 — Need User role</div>';document.getElementById('chat-send').disabled=false;return}
62
+ const reader=r.body.getReader();const dec=new TextDecoder();let div=document.createElement('div');div.className='msg agent';chat.appendChild(div);let buf=''
63
+ while(true){const{done,value}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:true});const lines=buf.split('\n');buf=lines.pop()||''
64
+ for(const line of lines){if(!line.startsWith('data: '))continue;try{const ev=JSON.parse(line.slice(6));if(ev.type==='text_delta')div.innerHTML+=ev.content.replace(/</g,'&lt;').replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>').replace(/\n/g,'<br>');else if(ev.type==='tool_call'){const t=document.createElement('div');t.className='msg tool';t.textContent='🔧 '+ev.toolName;chat.insertBefore(t,div)}else if(ev.type==='tool_result'){const t=document.createElement('div');t.className='msg tool';t.textContent='✅ '+(ev.output||'').substring(0,80);chat.insertBefore(t,div)}else if(ev.type==='done'){document.getElementById('chat-cost').textContent=(ev.usage?.totalTokens||0)+' tokens · '+(ev.durationMs||0)+'ms'+(ev.cost?' · $'+ev.cost.toFixed(6):'')}else if(ev.type==='error'){chat.innerHTML+='<div class="msg error">'+ev.message+'</div>'}}catch{}}
65
+ chat.scrollTop=chat.scrollHeight}loadTasks()}catch(e){chat.innerHTML+='<div class="msg error">'+e.message+'</div>'}document.getElementById('chat-send').disabled=false;chat.scrollTop=chat.scrollHeight}
66
+
67
+ loadTasks()
68
+ </script>
69
+ </body>
70
+ </html>
@@ -4,7 +4,11 @@
4
4
  * Uses the SAME guards and pipeline as HTTP controllers.
5
5
  * Tools from @Mixin(TaskTools) are available to the LLM.
6
6
  */
7
- import { Agent, MainLoop, Mixin, Memory, Budget, Hook } from '@theokit/agents'
7
+ import 'reflect-metadata'
8
+ import {
9
+ Agent, MainLoop, Mixin,
10
+ Memory, Budget, Hook,
11
+ } from '@theokit/agents'
8
12
  import { UseGuards, UseInterceptors } from '@theokit/http'
9
13
  import { RolesGuard, Roles, Role } from '../guards/auth.guard.js'
10
14
  import { TimingInterceptor } from '../interceptors/timing.interceptor.js'
@@ -21,7 +25,7 @@ Be concise and actionable.`,
21
25
  @UseInterceptors(TimingInterceptor)
22
26
  @Roles([Role.User])
23
27
  @Memory({ provider: 'built-in', scope: 'per-user' })
24
- @Budget({ maxCostUsd: 1.0, window: 'daily' })
28
+ @Budget({ maxCostUsd: 1.00, window: 'daily' })
25
29
  @Mixin(TaskTools)
26
30
  export class AssistantAgent {
27
31
  @MainLoop({ strategy: 'react', maxIterations: 5 })
@@ -5,20 +5,12 @@
5
5
  * @Param, @Query, @HttpCode, @UseGuards, @Roles, @IsPublic,
6
6
  * @UseInterceptors, @UseFilters, NotFoundException.
7
7
  */
8
+ import 'reflect-metadata'
8
9
  import { z } from 'zod'
9
10
  import {
10
- Controller,
11
- Get,
12
- Post,
13
- Put,
14
- Delete,
15
- Body,
16
- Param,
17
- Query,
18
- HttpCode,
19
- UseGuards,
20
- UseInterceptors,
21
- UseFilters,
11
+ Controller, Get, Post, Put, Delete,
12
+ Body, Param, Query, HttpCode,
13
+ UseGuards, UseInterceptors, UseFilters,
22
14
  NotFoundException,
23
15
  } from '@theokit/http'
24
16
  import { RolesGuard, Roles, Role, IsPublic } from '../guards/auth.guard.js'
@@ -31,7 +23,7 @@ const zCreateTask = z.object({
31
23
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
32
24
  })
33
25
 
34
- @Controller() // → /api/tasks (convention: inferred from TasksController)
26
+ @Controller() // → /api/tasks (convention: inferred from TasksController)
35
27
  @UseGuards(RolesGuard)
36
28
  @UseInterceptors(TimingInterceptor)
37
29
  @UseFilters(HttpErrorFilter)
@@ -0,0 +1,21 @@
1
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
2
+ import Database from 'better-sqlite3'
3
+ import { mkdirSync } from 'node:fs'
4
+ import * as schema from './schema.js'
5
+
6
+ mkdirSync('data', { recursive: true })
7
+ const sqlite = new Database('data/dev.db')
8
+ sqlite.pragma('journal_mode = WAL')
9
+
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
+ `)
@@ -0,0 +1,13 @@
1
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
2
+
3
+ export const tasks = sqliteTable('tasks', {
4
+ id: integer('id').primaryKey({ autoIncrement: true }),
5
+ title: text('title').notNull(),
6
+ priority: text('priority', { enum: ['low', 'medium', 'high'] })
7
+ .notNull()
8
+ .default('medium'),
9
+ done: integer('done', { mode: 'boolean' }).notNull().default(false),
10
+ createdAt: text('created_at')
11
+ .notNull()
12
+ .$defaultFn(() => new Date().toISOString().split('T')[0]),
13
+ })
@@ -0,0 +1,29 @@
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,18 +1,2 @@
1
- /**
2
- * Server module — auto-exports all controllers, agents, and providers.
3
- *
4
- * Convention: add your classes here. TheoApp.create() imports this one file.
5
- * Like Rails' application.rb — single entry point for the backend.
6
- *
7
- * When you run `theokit generate controller/agent/toolbox`, the generated
8
- * file is auto-added here.
9
- */
10
-
11
- // Controllers — HTTP API
12
- export { TasksController } from './controllers/tasks.controller.js'
13
-
14
- // Agents — AI endpoints
15
- export { AssistantAgent } from './agents/assistant.agent.js'
16
-
17
- // Providers — DI (toolboxes, services)
18
- export { TaskTools } from './toolboxes/task.tools.js'
1
+ // Routes are auto-discovered from server/routes/ by theokit dev.
2
+ // No manual registration needed — convention over configuration.
@@ -0,0 +1,36 @@
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
+ })
@@ -0,0 +1,20 @@
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
+ })
@@ -5,11 +5,12 @@
5
5
  * to interact with the task data. Each @Tool method is
6
6
  * compiled to a defineTool() call at startup.
7
7
  */
8
+ import 'reflect-metadata'
8
9
  import { z } from 'zod'
9
10
  import { Toolbox, Tool, Trace, Audit } from '@theokit/agents'
10
11
  import { taskStore } from '../store.js'
11
12
 
12
- @Toolbox() // Convention: TaskTools → namespace: 'task'
13
+ @Toolbox() // Convention: TaskTools → namespace: 'task'
13
14
  @Trace(true)
14
15
  export class TaskTools {
15
16
  @Tool({
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { z } from 'zod'
3
+
4
+ // Schema mirrors server/routes/tasks/index.ts POST body validation.
5
+ // Unit test — no server dependency.
6
+ const taskBodySchema = z.object({
7
+ title: z.string().min(3),
8
+ done: z.boolean().default(false),
9
+ })
10
+
11
+ describe('Task body validation', () => {
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)
18
+ }
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
+
35
+ it('rejects missing title', () => {
36
+ expect(taskBodySchema.safeParse({ done: true }).success).toBe(false)
37
+ })
38
+ })
@@ -1,11 +1,5 @@
1
1
  import { defineConfig } from 'theokit'
2
- import { httpDecoratorsPlugin } from '@theokit/http/theokit-plugin'
3
2
 
4
3
  export default defineConfig({
5
4
  port: 3000,
6
- plugins: [
7
- httpDecoratorsPlugin({
8
- controllersGlob: 'server/controllers/**/*.controller.ts',
9
- }),
10
- ],
11
5
  })