create-theokit 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) 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/globals.css +36 -0
  9. package/templates/default/app/page.tsx +72 -166
  10. package/templates/default/app.ts +24 -0
  11. package/templates/default/dot-claude/rules/theokit-conventions.md +36 -0
  12. package/templates/default/dot-claude/settings.json +23 -0
  13. package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +105 -0
  14. package/templates/default/dot-claude/skills/theokit-config/SKILL.md +128 -0
  15. package/templates/default/dot-claude/skills/theokit-database/SKILL.md +112 -0
  16. package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +89 -0
  17. package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +89 -0
  18. package/templates/default/drizzle.config.ts +10 -0
  19. package/templates/default/eslint.config.mjs +5 -1
  20. package/templates/default/package.json.tmpl +13 -3
  21. package/templates/default/public/index.html +70 -0
  22. package/templates/default/server/agents/assistant.agent.ts +6 -2
  23. package/templates/default/server/controllers/tasks.controller.ts +5 -13
  24. package/templates/default/server/db/index.ts +21 -0
  25. package/templates/default/server/db/schema.ts +13 -0
  26. package/templates/default/server/db/seed.ts +29 -0
  27. package/templates/default/server/index.ts +2 -18
  28. package/templates/default/server/routes/tasks/[id].ts +36 -0
  29. package/templates/default/server/routes/tasks/index.ts +20 -0
  30. package/templates/default/server/toolboxes/task.tools.ts +2 -1
  31. package/templates/default/tests/tasks.test.ts +10 -0
  32. package/templates/default/theo.config.ts +0 -6
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: theokit-database
3
+ description: TheoKit database — Drizzle ORM, SQLite schema, migrations, seeds, db commands
4
+ user-invocable: false
5
+ paths:
6
+ - "**/*schema*"
7
+ - "**/*db*"
8
+ - "**/drizzle*"
9
+ - "**/*migration*"
10
+ - "**/*seed*"
11
+ ---
12
+
13
+ # TheoKit Database (Drizzle + SQLite)
14
+
15
+ ## Schema Definition
16
+
17
+ ```typescript
18
+ // server/db/schema.ts
19
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
20
+
21
+ export const tasks = sqliteTable('tasks', {
22
+ id: integer('id').primaryKey({ autoIncrement: true }),
23
+ title: text('title').notNull(),
24
+ done: integer('done', { mode: 'boolean' }).notNull().default(false),
25
+ createdAt: text('created_at').notNull().default(sql`CURRENT_TIMESTAMP`),
26
+ })
27
+
28
+ export const users = sqliteTable('users', {
29
+ id: integer('id').primaryKey({ autoIncrement: true }),
30
+ email: text('email').notNull().unique(),
31
+ name: text('name').notNull(),
32
+ })
33
+ ```
34
+
35
+ ## DB Connection
36
+
37
+ ```typescript
38
+ // server/db/index.ts
39
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
40
+ import Database from 'better-sqlite3'
41
+ import * as schema from './schema.js'
42
+
43
+ const sqlite = new Database('data/dev.db')
44
+ export const db = drizzle(sqlite, { schema })
45
+ ```
46
+
47
+ ## Common Queries
48
+
49
+ ```typescript
50
+ import { db } from '@/server/db'
51
+ import { tasks } from '@/server/db/schema'
52
+ import { eq } from 'drizzle-orm'
53
+
54
+ // Select all
55
+ db.select().from(tasks).all()
56
+
57
+ // Select by ID
58
+ db.select().from(tasks).where(eq(tasks.id, 1)).get()
59
+
60
+ // Insert + return
61
+ db.insert(tasks).values({ title: 'New task' }).returning().get()
62
+
63
+ // Update
64
+ db.update(tasks).set({ done: true }).where(eq(tasks.id, 1)).run()
65
+
66
+ // Delete
67
+ db.delete(tasks).where(eq(tasks.id, 1)).run()
68
+ ```
69
+
70
+ ## Seeds
71
+
72
+ ```typescript
73
+ // server/db/seed.ts
74
+ import { db } from './index.js'
75
+ import { tasks } from './schema.js'
76
+
77
+ await db.insert(tasks).values([
78
+ { title: 'Learn TheoKit', done: false },
79
+ { title: 'Build an agent', done: false },
80
+ ]).run()
81
+
82
+ console.log('Seeded database')
83
+ ```
84
+
85
+ Run: `npm run seed` or `npx tsx server/db/seed.ts`
86
+
87
+ ## CLI Commands
88
+
89
+ ```bash
90
+ npx drizzle-kit push # Apply schema changes to dev DB (no migration files)
91
+ npx drizzle-kit generate # Generate SQL migration files (for production)
92
+ npm run seed # Run seed script
93
+ ```
94
+
95
+ ## Scaffolding Resources
96
+
97
+ ```bash
98
+ npx theokit generate resource posts title:string published:boolean
99
+ # Creates: server/db/schema.ts (appends table)
100
+ # server/routes/posts/index.ts (GET + POST)
101
+ # server/routes/posts/[id].ts (GET + PUT + DELETE)
102
+ # tests/posts.test.ts (smoke test)
103
+ ```
104
+
105
+ Supported field types: `string`, `text`, `number`, `boolean`
106
+
107
+ ## Anti-patterns
108
+
109
+ - NEVER use raw SQL for schema — use Drizzle's schema builder
110
+ - NEVER delete `data/dev.db` to apply changes — use `npx drizzle-kit push`
111
+ - NEVER put DB connection logic in route files — import from `server/db/index.ts`
112
+ - NEVER use `id`, `createdAt`, or `created_at` as field names in `generate resource` — they're auto-added
@@ -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,10 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ describe('Tasks API', () => {
4
+ it('GET /api/tasks returns array', async () => {
5
+ const res = await fetch('http://localhost:3000/api/tasks')
6
+ expect(res.status).toBe(200)
7
+ const tasks = await res.json()
8
+ expect(Array.isArray(tasks)).toBe(true)
9
+ })
10
+ })