create-theokit 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/AGENTS.md +41 -51
- package/templates/default/CLAUDE.md +25 -0
- package/templates/default/README.md.tmpl +53 -51
- package/templates/default/_gitignore +31 -0
- package/templates/default/app/page.tsx +78 -39
- package/templates/default/app.ts +24 -0
- package/templates/default/dot-claude/rules/theokit-conventions.md +36 -0
- package/templates/default/dot-claude/settings.json +23 -0
- package/templates/default/dot-claude/skills/theokit-agents/SKILL.md +105 -0
- package/templates/default/dot-claude/skills/theokit-config/SKILL.md +128 -0
- package/templates/default/dot-claude/skills/theokit-database/SKILL.md +112 -0
- package/templates/default/dot-claude/skills/theokit-frontend/SKILL.md +89 -0
- package/templates/default/dot-claude/skills/theokit-routes/SKILL.md +89 -0
- package/templates/default/drizzle.config.ts +10 -0
- package/templates/default/eslint.config.mjs +5 -1
- package/templates/default/package.json.tmpl +13 -3
- package/templates/default/public/index.html +70 -0
- package/templates/default/server/agents/assistant.agent.ts +6 -2
- package/templates/default/server/controllers/tasks.controller.ts +5 -13
- package/templates/default/server/db/index.ts +21 -0
- package/templates/default/server/db/schema.ts +13 -0
- package/templates/default/server/db/seed.ts +29 -0
- package/templates/default/server/index.ts +2 -18
- package/templates/default/server/routes/tasks/[id].ts +36 -0
- package/templates/default/server/routes/tasks/index.ts +20 -0
- package/templates/default/server/toolboxes/task.tools.ts +2 -1
- package/templates/default/tests/tasks.test.ts +10 -0
- package/templates/default/theo.config.ts +0 -6
|
@@ -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`
|
|
@@ -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
|
-
"@
|
|
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,'<')+'</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,'<').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
|
|
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.
|
|
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
|
-
|
|
12
|
-
|
|
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()
|
|
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
|
-
|
|
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()
|
|
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
|
+
})
|
|
@@ -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
|
})
|