create-theokit 0.4.0-beta.0 โ†’ 0.5.1

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 (108) hide show
  1. package/dist/cli.js +4 -3
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +8 -4
  4. package/templates/default/.env.example +5 -0
  5. package/templates/default/README.md.tmpl +71 -57
  6. package/templates/default/_gitignore +2 -3
  7. package/templates/default/app/layout.tsx +51 -82
  8. package/templates/default/app/page.tsx +169 -271
  9. package/templates/default/app.ts +29 -0
  10. package/templates/default/package.json.tmpl +9 -23
  11. package/templates/default/public/index.html +70 -0
  12. package/templates/default/server/agents/assistant.agent.ts +46 -0
  13. package/templates/default/server/controllers/tasks.controller.ts +70 -0
  14. package/templates/default/server/filters/http-error.filter.ts +20 -0
  15. package/templates/default/server/guards/auth.guard.ts +47 -0
  16. package/templates/default/server/interceptors/timing.interceptor.ts +14 -0
  17. package/templates/default/server/middleware/logger.middleware.ts +12 -0
  18. package/templates/default/server/store.ts +46 -0
  19. package/templates/default/server/toolboxes/task.tools.ts +58 -0
  20. package/templates/default/tsconfig.json +7 -7
  21. package/LICENSE +0 -201
  22. package/templates/api-only/.nvmrc +0 -1
  23. package/templates/api-only/README.md.tmpl +0 -78
  24. package/templates/api-only/_gitignore +0 -5
  25. package/templates/api-only/app/page.tsx +0 -3
  26. package/templates/api-only/index.html +0 -12
  27. package/templates/api-only/package.json.tmpl +0 -28
  28. package/templates/api-only/public/.gitkeep +0 -0
  29. package/templates/api-only/public/favicon.ico +0 -0
  30. package/templates/api-only/server/routes/health.ts +0 -5
  31. package/templates/api-only/server/routes/users.ts +0 -27
  32. package/templates/api-only/server/routes/webhooks/echo.ts +0 -34
  33. package/templates/api-only/theo.config.ts +0 -3
  34. package/templates/api-only/tsconfig.json +0 -15
  35. package/templates/dashboard/.nvmrc +0 -1
  36. package/templates/dashboard/README.md.tmpl +0 -76
  37. package/templates/dashboard/_gitignore +0 -5
  38. package/templates/dashboard/app/about/page.tsx +0 -3
  39. package/templates/dashboard/app/dashboard/layout.tsx +0 -10
  40. package/templates/dashboard/app/dashboard/page.tsx +0 -3
  41. package/templates/dashboard/app/layout.tsx +0 -14
  42. package/templates/dashboard/app/page.tsx +0 -8
  43. package/templates/dashboard/index.html +0 -12
  44. package/templates/dashboard/package.json.tmpl +0 -28
  45. package/templates/dashboard/public/.gitkeep +0 -0
  46. package/templates/dashboard/public/favicon.ico +0 -0
  47. package/templates/dashboard/server/crons/cleanup-conversations.ts +0 -59
  48. package/templates/dashboard/server/routes/health.ts +0 -5
  49. package/templates/dashboard/theo.config.ts +0 -3
  50. package/templates/dashboard/tsconfig.json +0 -15
  51. package/templates/default/.nvmrc +0 -1
  52. package/templates/default/index.html +0 -12
  53. package/templates/default/public/.gitkeep +0 -0
  54. package/templates/default/public/favicon.ico +0 -0
  55. package/templates/default/server/crons/cleanup-conversations.ts +0 -59
  56. package/templates/default/server/routes/chat.ts +0 -69
  57. package/templates/default/server/routes/health.ts +0 -5
  58. package/templates/default/theo.config.ts +0 -3
  59. package/templates/default/types/jobs.d.ts +0 -25
  60. package/templates/postgres/.env.example +0 -5
  61. package/templates/postgres/.nvmrc +0 -1
  62. package/templates/postgres/README.md.tmpl +0 -83
  63. package/templates/postgres/_gitignore +0 -5
  64. package/templates/postgres/app/layout.tsx +0 -14
  65. package/templates/postgres/app/page.tsx +0 -8
  66. package/templates/postgres/db/index.ts +0 -7
  67. package/templates/postgres/db/schema.ts +0 -8
  68. package/templates/postgres/drizzle.config.ts +0 -10
  69. package/templates/postgres/index.html +0 -12
  70. package/templates/postgres/package.json.tmpl +0 -36
  71. package/templates/postgres/public/.gitkeep +0 -0
  72. package/templates/postgres/public/favicon.ico +0 -0
  73. package/templates/postgres/server/context.ts +0 -5
  74. package/templates/postgres/server/jobs/log-message.ts +0 -26
  75. package/templates/postgres/server/routes/health.ts +0 -5
  76. package/templates/postgres/server/routes/users.ts +0 -22
  77. package/templates/postgres/theo.config.ts +0 -3
  78. package/templates/postgres/tsconfig.json +0 -15
  79. package/templates/saas/.env.example +0 -7
  80. package/templates/saas/.nvmrc +0 -1
  81. package/templates/saas/README.md.tmpl +0 -103
  82. package/templates/saas/_gitignore +0 -5
  83. package/templates/saas/app/layout.tsx +0 -5
  84. package/templates/saas/app/page.tsx +0 -104
  85. package/templates/saas/db/index.ts +0 -6
  86. package/templates/saas/db/schema.ts +0 -20
  87. package/templates/saas/drizzle.config.ts +0 -10
  88. package/templates/saas/index.html +0 -12
  89. package/templates/saas/package.json.tmpl +0 -38
  90. package/templates/saas/public/.gitkeep +0 -0
  91. package/templates/saas/public/favicon.ico +0 -0
  92. package/templates/saas/server/context.ts +0 -37
  93. package/templates/saas/server/routes/agent.ts +0 -49
  94. package/templates/saas/server/routes/billing/stripe-webhook.ts +0 -49
  95. package/templates/saas/server/routes/login.ts +0 -25
  96. package/templates/saas/server/routes/logout.ts +0 -10
  97. package/templates/saas/server/routes/me.ts +0 -10
  98. package/templates/saas/theo.config.ts +0 -5
  99. package/templates/saas/tsconfig.json +0 -15
  100. package/templates/services/agent-node/Dockerfile.tmpl +0 -20
  101. package/templates/services/agent-node/README.md +0 -38
  102. package/templates/services/agent-node/package.json.tmpl +0 -18
  103. package/templates/services/agent-node/src/index.ts +0 -58
  104. package/templates/services/agent-node/tsconfig.json +0 -13
  105. package/templates/services/agent-python/Dockerfile.tmpl +0 -20
  106. package/templates/services/agent-python/README.md +0 -37
  107. package/templates/services/agent-python/main.py +0 -77
  108. 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 scaffold โ€” an Agent Surface, composed entirely from TheoUI.
2
+ * Main page โ€” Task 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,29 @@
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 { readFileSync } from 'node:fs'
13
+ import { TheoApp } from '@theokit/http-decorators/app'
14
+ import { TasksController } from './server/controllers/tasks.controller.js'
15
+ import { AssistantAgent } from './server/agents/assistant.agent.js'
16
+ import { TaskTools } from './server/toolboxes/task.tools.js'
17
+
18
+ // Frontend HTML (inline for alpha โ€” upgrade to Vite plugin for React SSR)
19
+ let html: string | undefined
20
+ try { html = readFileSync(new URL('./public/index.html', import.meta.url), 'utf-8') } catch { /* no frontend */ }
21
+
22
+ const app = await TheoApp.create({
23
+ controllers: [TasksController],
24
+ agents: [AssistantAgent],
25
+ providers: [TaskTools],
26
+ html,
27
+ })
28
+
29
+ await app.listen(3000)
@@ -4,32 +4,18 @@
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.4.0-beta.0",
14
- "@theokit/sdk": "^1.6.2",
15
- "@theokit/ui": "^0.14.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/http-decorators": "^0.1.0",
13
+ "@theokit/agents": "^0.1.0",
14
+ "reflect-metadata": "^0.2.0",
15
+ "zod": "^3.22.0"
21
16
  },
22
17
  "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
- ]
18
+ "typescript": "^5.5.0",
19
+ "@swc/core": "^1.3.0"
34
20
  }
35
21
  }
@@ -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>
@@ -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
+ }