create-theokit 0.2.3 → 0.5.0

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 (104) hide show
  1. package/package.json +5 -1
  2. package/templates/default/.env.example +5 -0
  3. package/templates/default/README.md.tmpl +71 -57
  4. package/templates/default/_gitignore +2 -3
  5. package/templates/default/app/layout.tsx +51 -82
  6. package/templates/default/app/page.tsx +169 -271
  7. package/templates/default/app.ts +23 -0
  8. package/templates/default/package.json.tmpl +10 -23
  9. package/templates/default/server/agents/assistant.agent.ts +46 -0
  10. package/templates/default/server/controllers/tasks.controller.ts +70 -0
  11. package/templates/default/server/filters/http-error.filter.ts +20 -0
  12. package/templates/default/server/guards/auth.guard.ts +47 -0
  13. package/templates/default/server/interceptors/timing.interceptor.ts +14 -0
  14. package/templates/default/server/middleware/logger.middleware.ts +12 -0
  15. package/templates/default/server/store.ts +46 -0
  16. package/templates/default/server/toolboxes/task.tools.ts +58 -0
  17. package/templates/default/tsconfig.json +7 -7
  18. package/templates/api-only/.nvmrc +0 -1
  19. package/templates/api-only/README.md.tmpl +0 -78
  20. package/templates/api-only/_gitignore +0 -5
  21. package/templates/api-only/app/page.tsx +0 -3
  22. package/templates/api-only/index.html +0 -12
  23. package/templates/api-only/package.json.tmpl +0 -28
  24. package/templates/api-only/public/.gitkeep +0 -0
  25. package/templates/api-only/public/favicon.ico +0 -0
  26. package/templates/api-only/server/routes/health.ts +0 -5
  27. package/templates/api-only/server/routes/users.ts +0 -27
  28. package/templates/api-only/server/routes/webhooks/echo.ts +0 -34
  29. package/templates/api-only/theo.config.ts +0 -3
  30. package/templates/api-only/tsconfig.json +0 -15
  31. package/templates/dashboard/.nvmrc +0 -1
  32. package/templates/dashboard/README.md.tmpl +0 -76
  33. package/templates/dashboard/_gitignore +0 -5
  34. package/templates/dashboard/app/about/page.tsx +0 -3
  35. package/templates/dashboard/app/dashboard/layout.tsx +0 -10
  36. package/templates/dashboard/app/dashboard/page.tsx +0 -3
  37. package/templates/dashboard/app/layout.tsx +0 -14
  38. package/templates/dashboard/app/page.tsx +0 -8
  39. package/templates/dashboard/index.html +0 -12
  40. package/templates/dashboard/package.json.tmpl +0 -28
  41. package/templates/dashboard/public/.gitkeep +0 -0
  42. package/templates/dashboard/public/favicon.ico +0 -0
  43. package/templates/dashboard/server/crons/cleanup-conversations.ts +0 -59
  44. package/templates/dashboard/server/routes/health.ts +0 -5
  45. package/templates/dashboard/theo.config.ts +0 -3
  46. package/templates/dashboard/tsconfig.json +0 -15
  47. package/templates/default/.nvmrc +0 -1
  48. package/templates/default/index.html +0 -12
  49. package/templates/default/public/.gitkeep +0 -0
  50. package/templates/default/public/favicon.ico +0 -0
  51. package/templates/default/server/crons/cleanup-conversations.ts +0 -59
  52. package/templates/default/server/routes/chat.ts +0 -69
  53. package/templates/default/server/routes/health.ts +0 -5
  54. package/templates/default/theo.config.ts +0 -3
  55. package/templates/default/types/jobs.d.ts +0 -25
  56. package/templates/postgres/.env.example +0 -5
  57. package/templates/postgres/.nvmrc +0 -1
  58. package/templates/postgres/README.md.tmpl +0 -83
  59. package/templates/postgres/_gitignore +0 -5
  60. package/templates/postgres/app/layout.tsx +0 -14
  61. package/templates/postgres/app/page.tsx +0 -8
  62. package/templates/postgres/db/index.ts +0 -7
  63. package/templates/postgres/db/schema.ts +0 -8
  64. package/templates/postgres/drizzle.config.ts +0 -10
  65. package/templates/postgres/index.html +0 -12
  66. package/templates/postgres/package.json.tmpl +0 -36
  67. package/templates/postgres/public/.gitkeep +0 -0
  68. package/templates/postgres/public/favicon.ico +0 -0
  69. package/templates/postgres/server/context.ts +0 -5
  70. package/templates/postgres/server/jobs/log-message.ts +0 -26
  71. package/templates/postgres/server/routes/health.ts +0 -5
  72. package/templates/postgres/server/routes/users.ts +0 -22
  73. package/templates/postgres/theo.config.ts +0 -3
  74. package/templates/postgres/tsconfig.json +0 -15
  75. package/templates/saas/.env.example +0 -7
  76. package/templates/saas/.nvmrc +0 -1
  77. package/templates/saas/README.md.tmpl +0 -103
  78. package/templates/saas/_gitignore +0 -5
  79. package/templates/saas/app/layout.tsx +0 -5
  80. package/templates/saas/app/page.tsx +0 -104
  81. package/templates/saas/db/index.ts +0 -6
  82. package/templates/saas/db/schema.ts +0 -20
  83. package/templates/saas/drizzle.config.ts +0 -10
  84. package/templates/saas/index.html +0 -12
  85. package/templates/saas/package.json.tmpl +0 -38
  86. package/templates/saas/public/.gitkeep +0 -0
  87. package/templates/saas/public/favicon.ico +0 -0
  88. package/templates/saas/server/context.ts +0 -37
  89. package/templates/saas/server/routes/agent.ts +0 -49
  90. package/templates/saas/server/routes/billing/stripe-webhook.ts +0 -49
  91. package/templates/saas/server/routes/login.ts +0 -25
  92. package/templates/saas/server/routes/logout.ts +0 -10
  93. package/templates/saas/server/routes/me.ts +0 -10
  94. package/templates/saas/theo.config.ts +0 -5
  95. package/templates/saas/tsconfig.json +0 -15
  96. package/templates/services/agent-node/Dockerfile.tmpl +0 -20
  97. package/templates/services/agent-node/README.md +0 -38
  98. package/templates/services/agent-node/package.json.tmpl +0 -18
  99. package/templates/services/agent-node/src/index.ts +0 -58
  100. package/templates/services/agent-node/tsconfig.json +0 -13
  101. package/templates/services/agent-python/Dockerfile.tmpl +0 -20
  102. package/templates/services/agent-python/README.md +0 -37
  103. package/templates/services/agent-python/main.py +0 -77
  104. package/templates/services/agent-python/pyproject.toml.tmpl +0 -16
@@ -1,285 +1,183 @@
1
- 'use client'
2
-
3
- import { useEffect, useMemo, useState } from 'react'
4
- import {
5
- ChatThread,
6
- ChatMessage,
7
- ChatComposer,
8
- ToolCallCard,
9
- AgentStreaming,
10
- AgentErrorCard,
11
- EmptyState,
12
- QuickActionChips,
13
- ContextWindowBar,
14
- CommandPalette,
15
- Avatar,
16
- Tooltip,
17
- Button,
18
- ScrollArea,
19
- type UIMessage,
20
- type QuickAction,
21
- type CommandItem,
22
- type ToolCallStatus,
23
- } from '@theokit/ui'
24
- import { Sparkles, Wrench, RotateCcw, Command } from 'lucide-react'
25
- import { useAgentStream } from 'theokit/client'
26
-
27
1
  /**
28
- * Default scaffoldan Agent Surface, composed entirely from TheoUI.
2
+ * Main pageTask Manager + AI Chat.
29
3
  *
30
- * ChatThread / ChatMessage → conversation
31
- * ToolCallCard → expandable tool invocations
32
- * AgentStreaming → streaming indicator
33
- * AgentErrorCard → error display
34
- * ChatComposer → bottom input bar
35
- * EmptyState → first-load screen
36
- * ContextWindowBar → context usage at top
37
- * CommandPalette → ⌘K quick actions
38
- * Avatar → assistant face in messages
39
- * Tooltip → hints on icons
40
- *
41
- * `useAgentStream` handles SSE consumption, AbortController cleanup, and
42
- * StrictMode safety. Replace the mock at server/routes/chat.ts with your
43
- * real LLM provider (OpenAI / Anthropic / local).
4
+ * Split layout: left side CRUD, right side AI agent chat.
5
+ * SSE streaming renders token-by-token with tool call visualization.
44
6
  */
7
+ export default function Page() {
8
+ return (
9
+ <div id="app">
10
+ <header>
11
+ <h1><span className="accent">TheoKit</span> Task Manager</h1>
12
+ <p className="subtitle">HTTP Controllers + AI Agent — same pipeline, same guards</p>
13
+ <div className="role-bar">
14
+ <label>Role: </label>
15
+ <select id="role">
16
+ <option value="">None (public only)</option>
17
+ <option value="user" selected>User</option>
18
+ <option value="admin">Admin</option>
19
+ </select>
20
+ </div>
21
+ </header>
22
+
23
+ <main className="grid">
24
+ {/* Left: CRUD Panel */}
25
+ <section className="card">
26
+ <h2>📋 Tasks <span className="badge">@Controller</span></h2>
27
+ <table>
28
+ <thead><tr><th>Task</th><th>Priority</th><th>Status</th></tr></thead>
29
+ <tbody id="task-list"></tbody>
30
+ </table>
31
+ <form id="create-form" className="create-bar">
32
+ <input id="new-title" placeholder="New task..." required minLength={3} />
33
+ <select id="new-priority">
34
+ <option value="medium">Medium</option>
35
+ <option value="high">High</option>
36
+ <option value="low">Low</option>
37
+ </select>
38
+ <button type="submit">Add</button>
39
+ </form>
40
+ <p id="form-error" className="error"></p>
41
+ </section>
42
+
43
+ {/* Right: AI Chat */}
44
+ <section className="card">
45
+ <h2>🤖 AI Assistant <span className="badge badge-ai">@Agent + SSE</span></h2>
46
+ <div id="chat" className="chat-box">
47
+ <div className="msg system">Ask me to list, create, or complete tasks...</div>
48
+ </div>
49
+ <div className="chat-bar">
50
+ <input id="chat-input" placeholder="Message the AI assistant..." />
51
+ <button id="chat-send" onClick={() => {}}>Send</button>
52
+ </div>
53
+ <p id="chat-cost" className="cost"></p>
54
+ </section>
55
+ </main>
56
+
57
+ <ClientScript />
58
+ </div>
59
+ )
60
+ }
45
61
 
46
- type ConversationItem =
47
- | { kind: 'message'; id: string; role: 'user' | 'assistant'; content: string; timestamp: string }
48
- | {
49
- kind: 'tool'
50
- id: string
51
- tool: string
52
- target?: string
53
- status: ToolCallStatus
54
- output?: string
55
- timestamp: string
56
- }
57
- | { kind: 'error'; id: string; message: string; timestamp: string }
58
-
59
- const QUICK_ACTIONS: QuickAction[] = [
60
- { id: 'summarize', label: 'Summarize this page', icon: Sparkles },
61
- { id: 'tools', label: 'Show available tools', icon: Wrench },
62
- { id: 'reset', label: 'Start a new conversation', icon: RotateCcw },
63
- ]
64
-
65
- const COMMAND_ITEMS: CommandItem[] = QUICK_ACTIONS.map((a) => ({
66
- id: a.id,
67
- label: a.label,
68
- icon: a.icon,
69
- group: 'Quick actions',
70
- }))
71
-
72
- // Mock context-window usage — replace with real model state.
73
- const CONTEXT_USED = 4_200
74
- const CONTEXT_TOTAL = 200_000
75
- const MODEL_NAME = 'mock-llm'
76
-
77
- // Modern chat UX: only the assistant carries an avatar. User messages are
78
- // right-aligned with a distinct bubble style — that's enough signal.
79
- // (TheoUI's ChatMessage uses flex-col, so a user avatar would land BELOW
80
- // the bubble, not above — visually unusual.)
81
- const ASSISTANT_AVATAR = (
82
- <Avatar size="sm" tone="primary">
83
- <Avatar.Fallback>TH</Avatar.Fallback>
84
- </Avatar>
85
- )
62
+ function ClientScript() {
63
+ return (
64
+ <script dangerouslySetInnerHTML={{ __html: CLIENT_JS }} />
65
+ )
66
+ }
86
67
 
87
- export default function Page() {
88
- const [composerValue, setComposerValue] = useState('')
89
- const [userMessages, setUserMessages] = useState<ConversationItem[]>([])
90
- const [paletteOpen, setPaletteOpen] = useState(false)
91
- const { events, send, status, reset } = useAgentStream<{ message: string }>('/api/chat')
68
+ const CLIENT_JS = `
69
+ const API = '';
70
+ let sessionId = 'session-' + Date.now();
71
+ const getRole = () => document.getElementById('role').value;
72
+ const headers = () => {
73
+ const h = { 'Content-Type': 'application/json' };
74
+ const r = getRole();
75
+ if (r) h['x-role'] = r;
76
+ return h;
77
+ };
78
+
79
+ // ─── Tasks CRUD ─────────────────────────────────────
80
+
81
+ async function loadTasks() {
82
+ const res = await fetch(API + '/api/tasks');
83
+ const tasks = await res.json();
84
+ document.getElementById('task-list').innerHTML = tasks.map(t => {
85
+ const statusClass = t.done ? 'done' : 'pending';
86
+ const prioClass = t.priority === 'high' ? 'prio-high' : t.priority === 'low' ? 'prio-low' : 'prio-med';
87
+ return '<tr class="' + statusClass + '"><td>' + (t.done ? '✅ ' : '○ ') + t.title + '</td><td><span class="prio ' + prioClass + '">' + t.priority + '</span></td><td>' + (t.done ? 'Done' : 'To do') + '</td></tr>';
88
+ }).join('');
89
+ }
92
90
 
93
- // ⌘K / Ctrl+K opens the CommandPalette.
94
- useEffect(() => {
95
- function onKey(e: KeyboardEvent) {
96
- if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
97
- e.preventDefault()
98
- setPaletteOpen((v) => !v)
99
- }
91
+ document.getElementById('create-form').addEventListener('submit', async (e) => {
92
+ e.preventDefault();
93
+ const title = document.getElementById('new-title').value.trim();
94
+ const priority = document.getElementById('new-priority').value;
95
+ const err = document.getElementById('form-error');
96
+ err.textContent = '';
97
+ if (!title) return;
98
+ const res = await fetch(API + '/api/tasks', { method: 'POST', headers: headers(), body: JSON.stringify({ title, priority }) });
99
+ if (res.status === 403) { err.textContent = '403 — Need User role'; return; }
100
+ if (res.status === 422) { const e = await res.json(); err.textContent = e.error?.issues?.[0]?.message || 'Validation error'; return; }
101
+ if (!res.ok) { err.textContent = 'Error ' + res.status; return; }
102
+ document.getElementById('new-title').value = '';
103
+ loadTasks();
104
+ });
105
+
106
+ // ─── AI Chat (SSE) ──────────────────────────────────
107
+
108
+ document.getElementById('chat-send').addEventListener('click', sendChat);
109
+ document.getElementById('chat-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChat(); });
110
+
111
+ async function sendChat() {
112
+ const input = document.getElementById('chat-input');
113
+ const msg = input.value.trim();
114
+ if (!msg) return;
115
+ input.value = '';
116
+
117
+ const chat = document.getElementById('chat');
118
+ chat.innerHTML += '<div class="msg user">You: ' + escapeHtml(msg) + '</div>';
119
+ document.getElementById('chat-send').disabled = true;
120
+
121
+ try {
122
+ const res = await fetch(API + '/api/agents/assistant/chat', {
123
+ method: 'POST', headers: headers(),
124
+ body: JSON.stringify({ message: msg, sessionId })
125
+ });
126
+
127
+ if (res.status === 403) {
128
+ chat.innerHTML += '<div class="msg system">403 — Need User role to chat</div>';
129
+ document.getElementById('chat-send').disabled = false;
130
+ return;
100
131
  }
101
- window.addEventListener('keydown', onKey)
102
- return () => window.removeEventListener('keydown', onKey)
103
- }, [])
104
132
 
105
- const items = useMemo<ConversationItem[]>(() => {
106
- const ts = new Date().toISOString()
107
- const agentItems: ConversationItem[] = events.map((event, i) => {
108
- const id = `e-${i}`
109
- switch (event.type) {
110
- case 'message':
111
- return { kind: 'message', id, role: 'assistant', content: event.content, timestamp: ts }
112
- case 'tool_call':
113
- return {
114
- kind: 'tool',
115
- id,
116
- tool: event.name,
117
- target:
118
- typeof event.args === 'object' && event.args !== null
119
- ? Object.entries(event.args as Record<string, unknown>)
120
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
121
- .join(' ')
122
- : undefined,
123
- status: 'running',
124
- timestamp: ts,
125
- }
126
- case 'tool_result':
127
- return {
128
- kind: 'tool',
129
- id,
130
- tool: event.name,
131
- status: 'success',
132
- output:
133
- typeof event.data === 'string' ? event.data : JSON.stringify(event.data, null, 2),
134
- timestamp: ts,
133
+ const reader = res.body.getReader();
134
+ const decoder = new TextDecoder();
135
+ let agentDiv = document.createElement('div');
136
+ agentDiv.className = 'msg agent';
137
+ agentDiv.textContent = '';
138
+ chat.appendChild(agentDiv);
139
+
140
+ let buffer = '';
141
+ while (true) {
142
+ const { done, value } = await reader.read();
143
+ if (done) break;
144
+ buffer += decoder.decode(value, { stream: true });
145
+ const lines = buffer.split('\\n');
146
+ buffer = lines.pop() || '';
147
+ for (const line of lines) {
148
+ if (!line.startsWith('data: ')) continue;
149
+ try {
150
+ const ev = JSON.parse(line.slice(6));
151
+ if (ev.type === 'text_delta') {
152
+ agentDiv.innerHTML += formatMarkdown(ev.content);
153
+ } else if (ev.type === 'tool_call') {
154
+ chat.insertBefore(toolMsg('🔧 Calling: ' + ev.toolName), agentDiv);
155
+ } else if (ev.type === 'tool_result') {
156
+ chat.insertBefore(toolMsg('✅ ' + (ev.output || '').substring(0, 80)), agentDiv);
157
+ } else if (ev.type === 'thinking') {
158
+ chat.insertBefore(sysMsg('💭 ' + ev.content), agentDiv);
159
+ } else if (ev.type === 'done') {
160
+ const cost = ev.cost ? ' · $' + ev.cost.toFixed(6) : '';
161
+ document.getElementById('chat-cost').textContent = ev.usage.totalTokens + ' tokens · ' + ev.durationMs + 'ms' + cost;
162
+ } else if (ev.type === 'error') {
163
+ chat.innerHTML += '<div class="msg error">' + ev.message + '</div>';
135
164
  }
136
- case 'error':
137
- return { kind: 'error', id, message: event.message, timestamp: ts }
165
+ } catch {}
138
166
  }
139
- })
140
- return [...userMessages, ...agentItems]
141
- }, [userMessages, events])
142
-
143
- function handleSubmit(value: string) {
144
- const trimmed = value.trim()
145
- if (!trimmed) return
146
- const id = `u-${userMessages.length}`
147
- setUserMessages((prev) => [
148
- ...prev,
149
- { kind: 'message', id, role: 'user', content: trimmed, timestamp: new Date().toISOString() },
150
- ])
151
- send({ message: trimmed })
152
- setComposerValue('')
153
- }
154
-
155
- function handleQuickAction(id: string) {
156
- setPaletteOpen(false)
157
- if (id === 'reset') {
158
- setUserMessages([])
159
- reset()
160
- return
167
+ chat.scrollTop = chat.scrollHeight;
161
168
  }
162
- const action = QUICK_ACTIONS.find((a) => a.id === id)
163
- if (action) handleSubmit(typeof action.label === 'string' ? action.label : '')
169
+ loadTasks(); // refresh after agent actions
170
+ } catch (e) {
171
+ chat.innerHTML += '<div class="msg error">Error: ' + e.message + '</div>';
164
172
  }
173
+ document.getElementById('chat-send').disabled = false;
174
+ chat.scrollTop = chat.scrollHeight;
175
+ }
165
176
 
166
- const isStreaming = status === 'streaming'
167
- const isEmpty = items.length === 0 && !isStreaming
168
- const hasError = status === 'error'
169
-
170
- return (
171
- <>
172
- <ContextWindowBar
173
- used={CONTEXT_USED}
174
- total={CONTEXT_TOTAL}
175
- trailing={MODEL_NAME}
176
- label="Context window"
177
- compact
178
- className="border-border/60 border-b px-6 py-2"
179
- />
180
-
181
- <ScrollArea className="flex-1">
182
- <div className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-6">
183
- {isEmpty ? (
184
- <EmptyState
185
- eyebrow="Theo Agent"
186
- icon={Sparkles}
187
- title="What should we build today?"
188
- description="Ask anything. This scaffold ships with a mock LLM at server/routes/chat.ts so you can see the wiring before plugging in a real model."
189
- action={<QuickActionChips actions={QUICK_ACTIONS} onSelect={handleQuickAction} />}
190
- />
191
- ) : (
192
- <ChatThread>
193
- {items.map((item) => {
194
- if (item.kind === 'message') {
195
- const message: UIMessage = {
196
- id: item.id,
197
- role: item.role,
198
- parts: [{ type: 'text', text: item.content, state: 'done' }],
199
- }
200
- return (
201
- <ChatMessage
202
- key={item.id}
203
- message={message}
204
- avatar={item.role === 'assistant' ? ASSISTANT_AVATAR : undefined}
205
- />
206
- )
207
- }
208
- if (item.kind === 'tool') {
209
- return (
210
- <ToolCallCard
211
- key={item.id}
212
- tool={item.tool}
213
- icon={Wrench}
214
- target={item.target}
215
- status={item.status}
216
- output={item.output}
217
- timestamp={item.timestamp}
218
- />
219
- )
220
- }
221
- return (
222
- <AgentErrorCard
223
- key={item.id}
224
- kind="tool-failure"
225
- title="Agent error"
226
- detail={item.message}
227
- />
228
- )
229
- })}
230
- {isStreaming && <AgentStreaming model={MODEL_NAME} />}
231
- </ChatThread>
232
- )}
233
- </div>
234
- </ScrollArea>
235
-
236
- <div className="border-border/60 border-t bg-background/50 backdrop-blur">
237
- <div className="mx-auto w-full max-w-3xl px-6 py-4">
238
- {hasError && (
239
- <div className="mb-3">
240
- <AgentErrorCard
241
- kind="network"
242
- title="Stream ended with an error"
243
- detail="The connection to the agent endpoint was interrupted. Reset to try again."
244
- actions={
245
- <Button variant="ghost" size="sm" onClick={() => reset()}>
246
- Reset
247
- </Button>
248
- }
249
- />
250
- </div>
251
- )}
252
- <ChatComposer
253
- value={composerValue}
254
- onValueChange={setComposerValue}
255
- onSubmit={handleSubmit}
256
- running={isStreaming}
257
- placeholder="Ask the agent…"
258
- leadingActions={
259
- <Tooltip label="Open command palette (⌘K)" side="top">
260
- <Button
261
- type="button"
262
- variant="ghost"
263
- size="icon"
264
- onClick={() => setPaletteOpen(true)}
265
- aria-label="Open command palette"
266
- >
267
- <Command className="size-4" />
268
- </Button>
269
- </Tooltip>
270
- }
271
- />
272
- </div>
273
- </div>
177
+ function toolMsg(text) { const d = document.createElement('div'); d.className = 'msg tool'; d.textContent = text; return d; }
178
+ function sysMsg(text) { const d = document.createElement('div'); d.className = 'msg system'; d.textContent = text; return d; }
179
+ function escapeHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
180
+ function formatMarkdown(s) { return escapeHtml(s).replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>').replace(/\\n/g, '<br>'); }
274
181
 
275
- <CommandPalette
276
- open={paletteOpen}
277
- onOpenChange={setPaletteOpen}
278
- items={COMMAND_ITEMS}
279
- onSelect={handleQuickAction}
280
- placeholder="Run a command…"
281
- emptyMessage="No matching commands."
282
- />
283
- </>
284
- )
285
- }
182
+ loadTasks();
183
+ `
@@ -0,0 +1,23 @@
1
+ /**
2
+ * TheoKit App — entry point.
3
+ *
4
+ * That's it. No manual wiring. No plumbing.
5
+ * TheoApp.create() handles everything:
6
+ * - Controller routes (HTTP CRUD)
7
+ * - Agent routes (SSE streaming + tool calling)
8
+ * - DI (providers injected into controllers + agents)
9
+ * - Shared pipeline (guards, interceptors, filters)
10
+ */
11
+ import 'reflect-metadata'
12
+ import { TheoApp } from '@theokit/http-decorators/app'
13
+ import { TasksController } from './server/controllers/tasks.controller.js'
14
+ import { AssistantAgent } from './server/agents/assistant.agent.js'
15
+ import { TaskTools } from './server/toolboxes/task.tools.js'
16
+
17
+ const app = await TheoApp.create({
18
+ controllers: [TasksController],
19
+ agents: [AssistantAgent],
20
+ providers: [TaskTools],
21
+ })
22
+
23
+ await app.listen(3000)
@@ -4,32 +4,19 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "theokit dev",
8
- "build": "theokit build",
9
- "start": "theokit start",
10
- "typecheck": "tsc --noEmit"
7
+ "dev": "bun app.ts",
8
+ "dev:node": "npx tsx app.ts",
9
+ "test": "bun test"
11
10
  },
12
11
  "dependencies": {
13
- "theokit": "^0.2.2",
14
- "@theokit/sdk": "^1.5.0",
15
- "@theokit/ui": "^0.13.0",
16
- "lucide-react": "^0.469.0",
17
- "react": "^19.0.0",
18
- "react-dom": "^19.0.0",
19
- "react-router": "^7.0.0",
20
- "zod": "^3.24.0"
12
+ "theokit": "^0.4.0",
13
+ "@theokit/http-decorators": "^0.1.0",
14
+ "@theokit/agents": "^0.1.0",
15
+ "reflect-metadata": "^0.2.0",
16
+ "zod": "^3.22.0"
21
17
  },
22
18
  "devDependencies": {
23
- "@types/node": "^22.10.0",
24
- "typescript": "^5.7.0",
25
- "@types/react": "^19.0.0",
26
- "@types/react-dom": "^19.0.0",
27
- "tailwindcss": "^4.0.0",
28
- "@tailwindcss/vite": "^4.0.0"
29
- },
30
- "pnpm": {
31
- "onlyBuiltDependencies": [
32
- "esbuild"
33
- ]
19
+ "typescript": "^5.5.0",
20
+ "@swc/core": "^1.3.0"
34
21
  }
35
22
  }
@@ -0,0 +1,46 @@
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-decorators'
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
+ name: 'assistant',
19
+ route: '/api/agents/assistant',
20
+ model: 'openai/gpt-4o-mini',
21
+ systemPrompt: `You are a helpful task management assistant.
22
+ Use the tasks.* tools to list, search, create, and complete tasks.
23
+ Be concise and actionable.`,
24
+ stream: true,
25
+ maxIterations: 5,
26
+ })
27
+ @UseGuards(RolesGuard)
28
+ @UseInterceptors(TimingInterceptor)
29
+ @Roles([Role.User])
30
+ @Memory({ provider: 'built-in', scope: 'per-user' })
31
+ @Budget({ maxCostUsd: 1.00, window: 'daily' })
32
+ @Mixin(TaskTools)
33
+ export class AssistantAgent {
34
+ @MainLoop({ strategy: 'react', maxIterations: 5 })
35
+ async run() {}
36
+
37
+ @Hook('before:llm-call')
38
+ async onBeforeLLM() {
39
+ console.log(' 🧠 Agent thinking...')
40
+ }
41
+
42
+ @Hook('after:tool-call')
43
+ async onToolDone() {
44
+ console.log(' 🔧 Tool executed')
45
+ }
46
+ }
@@ -0,0 +1,70 @@
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-decorators'
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')
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
+ }
@@ -0,0 +1,20 @@
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-decorators'
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
+ }