@voltx/cli 0.3.2 → 0.3.4

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.
@@ -0,0 +1,497 @@
1
+ import {
2
+ printWelcomeBanner
3
+ } from "./chunk-IV352HZA.mjs";
4
+ import {
5
+ __require
6
+ } from "./chunk-Y6FXYEAI.mjs";
7
+
8
+ // src/create.ts
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ var V = "^0.3.0";
12
+ var TEMPLATE_DEPS = {
13
+ blank: { "@voltx/core": V, "@voltx/server": V },
14
+ chatbot: { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/memory": V },
15
+ "rag-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/rag": V, "@voltx/db": V },
16
+ "agent-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/agents": V, "@voltx/memory": V }
17
+ };
18
+ async function createProject(options) {
19
+ const { name, template = "blank", auth = "none" } = options;
20
+ const targetDir = path.resolve(process.cwd(), name);
21
+ if (fs.existsSync(targetDir)) {
22
+ console.error(`[voltx] Directory "${name}" already exists.`);
23
+ process.exit(1);
24
+ }
25
+ fs.mkdirSync(targetDir, { recursive: true });
26
+ const provider = template === "rag-app" ? "openai" : "cerebras";
27
+ const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
28
+ const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
29
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": V };
30
+ if (auth === "better-auth") {
31
+ deps["@voltx/auth"] = V;
32
+ deps["better-auth"] = "^1.5.0";
33
+ } else if (auth === "jwt") {
34
+ deps["@voltx/auth"] = V;
35
+ deps["jose"] = "^6.0.0";
36
+ }
37
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
38
+ name,
39
+ version: "0.1.0",
40
+ private: true,
41
+ scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
42
+ dependencies: deps,
43
+ devDependencies: { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" }
44
+ }, null, 2));
45
+ let config = `import { defineConfig } from "@voltx/core";
46
+
47
+ export default defineConfig({
48
+ name: "${name}",
49
+ port: 3000,
50
+ ai: {
51
+ provider: "${provider}",
52
+ model: "${model}",
53
+ },`;
54
+ if (hasDb) config += `
55
+ db: {
56
+ url: process.env.DATABASE_URL,
57
+ },`;
58
+ if (auth !== "none") config += `
59
+ auth: {
60
+ provider: "${auth}",
61
+ },`;
62
+ config += `
63
+ server: {
64
+ routesDir: "src/routes",
65
+ staticDir: "public",
66
+ cors: true,
67
+ },
68
+ });
69
+ `;
70
+ fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
71
+ fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
72
+ fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
73
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify({
74
+ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", strict: true, esModuleInterop: true, skipLibCheck: true, outDir: "dist" },
75
+ include: ["src", "voltx.config.ts"]
76
+ }, null, 2));
77
+ fs.writeFileSync(
78
+ path.join(targetDir, "src", "index.ts"),
79
+ `import { createApp } from "@voltx/core";
80
+ import config from "../voltx.config";
81
+
82
+ const app = createApp(config);
83
+ app.start();
84
+ `
85
+ );
86
+ fs.writeFileSync(
87
+ path.join(targetDir, "src", "routes", "index.ts"),
88
+ `// GET / \u2014 Health check
89
+ import type { Context } from "@voltx/server";
90
+
91
+ export function GET(c: Context) {
92
+ return c.json({ name: "${name}", status: "ok" });
93
+ }
94
+ `
95
+ );
96
+ if (template === "chatbot" || template === "agent-app") {
97
+ fs.writeFileSync(
98
+ path.join(targetDir, "src", "routes", "api", "chat.ts"),
99
+ `// POST /api/chat \u2014 Streaming chat with conversation memory
100
+ import type { Context } from "@voltx/server";
101
+ import { streamText } from "@voltx/ai";
102
+ import { createMemory } from "@voltx/memory";
103
+
104
+ const memory = createMemory({ maxMessages: 50 });
105
+
106
+ export async function POST(c: Context) {
107
+ const { messages, conversationId = "default" } = await c.req.json();
108
+
109
+ const lastMessage = messages[messages.length - 1];
110
+ if (lastMessage?.role === "user") {
111
+ await memory.add(conversationId, { role: "user", content: lastMessage.content });
112
+ }
113
+
114
+ const history = await memory.get(conversationId);
115
+
116
+ const result = await streamText({
117
+ model: "${provider}:${model}",
118
+ system: "You are a helpful AI assistant.",
119
+ messages: history.map((m) => ({ role: m.role, content: m.content })),
120
+ });
121
+
122
+ result.text.then(async (text) => {
123
+ await memory.add(conversationId, { role: "assistant", content: text });
124
+ });
125
+
126
+ return result.toSSEResponse();
127
+ }
128
+ `
129
+ );
130
+ }
131
+ if (template === "agent-app") {
132
+ fs.mkdirSync(path.join(targetDir, "src", "agents"), { recursive: true });
133
+ fs.mkdirSync(path.join(targetDir, "src", "tools"), { recursive: true });
134
+ fs.writeFileSync(path.join(targetDir, "src", "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
135
+ import type { Tool } from "@voltx/agents";
136
+
137
+ export const calculatorTool: Tool = {
138
+ name: "calculator",
139
+ description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
140
+ parameters: {
141
+ type: "object",
142
+ properties: { expression: { type: "string", description: "The math expression to evaluate" } },
143
+ required: ["expression"],
144
+ },
145
+ async execute(args: { expression: string }) {
146
+ try {
147
+ const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
148
+ if (args.expression.includes("Math.")) return match;
149
+ throw new Error("Invalid character: " + match);
150
+ });
151
+ const result = new Function("return " + safe)();
152
+ return \`\${args.expression} = \${result}\`;
153
+ } catch (err) {
154
+ return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
155
+ }
156
+ },
157
+ };
158
+ `);
159
+ fs.writeFileSync(path.join(targetDir, "src", "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
160
+ import type { Tool } from "@voltx/agents";
161
+
162
+ export const datetimeTool: Tool = {
163
+ name: "datetime",
164
+ description: "Get the current date, time, day of week, and timezone.",
165
+ parameters: {
166
+ type: "object",
167
+ properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
168
+ },
169
+ async execute(args: { timezone?: string }) {
170
+ const now = new Date();
171
+ const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
172
+ const formatted = now.toLocaleString("en-US", {
173
+ timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
174
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
175
+ });
176
+ return \`Current date/time (\${tz}): \${formatted}\`;
177
+ },
178
+ };
179
+ `);
180
+ fs.writeFileSync(path.join(targetDir, "src", "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
181
+ import { createAgent } from "@voltx/agents";
182
+ import { calculatorTool } from "../tools/calculator";
183
+ import { datetimeTool } from "../tools/datetime";
184
+
185
+ export const assistant = createAgent({
186
+ name: "assistant",
187
+ model: "${provider}:${model}",
188
+ instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
189
+ tools: [calculatorTool, datetimeTool],
190
+ maxIterations: 5,
191
+ });
192
+ `);
193
+ fs.writeFileSync(
194
+ path.join(targetDir, "src", "routes", "api", "agent.ts"),
195
+ `// POST /api/agent \u2014 Run the AI agent
196
+ import type { Context } from "@voltx/server";
197
+ import { assistant } from "../../agents/assistant";
198
+
199
+ export async function POST(c: Context) {
200
+ const { input } = await c.req.json();
201
+ if (!input) return c.json({ error: "Missing 'input' field" }, 400);
202
+ const result = await assistant.run(input);
203
+ return c.json({ content: result.content, steps: result.steps });
204
+ }
205
+ `
206
+ );
207
+ }
208
+ if (template === "rag-app") {
209
+ const embedModel = "openai:text-embedding-3-small";
210
+ fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "rag"), { recursive: true });
211
+ fs.writeFileSync(
212
+ path.join(targetDir, "src", "routes", "api", "rag", "query.ts"),
213
+ `// POST /api/rag/query \u2014 Query documents with RAG
214
+ import type { Context } from "@voltx/server";
215
+ import { streamText } from "@voltx/ai";
216
+ import { createRAGPipeline, createEmbedder } from "@voltx/rag";
217
+ import { createVectorStore } from "@voltx/db";
218
+
219
+ const vectorStore = createVectorStore();
220
+ const embedder = createEmbedder({ model: "${embedModel}" });
221
+ const rag = createRAGPipeline({ embedder, vectorStore });
222
+
223
+ export async function POST(c: Context) {
224
+ const { question } = await c.req.json();
225
+ const context = await rag.getContext(question, { topK: 5 });
226
+ const result = await streamText({
227
+ model: "${provider}:${model}",
228
+ system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
229
+ messages: [{ role: "user", content: question }],
230
+ });
231
+ return result.toSSEResponse();
232
+ }
233
+ `
234
+ );
235
+ fs.writeFileSync(
236
+ path.join(targetDir, "src", "routes", "api", "rag", "ingest.ts"),
237
+ `// POST /api/rag/ingest \u2014 Ingest documents into the vector store
238
+ import type { Context } from "@voltx/server";
239
+ import { createRAGPipeline, createEmbedder } from "@voltx/rag";
240
+ import { createVectorStore } from "@voltx/db";
241
+
242
+ const vectorStore = createVectorStore();
243
+ const embedder = createEmbedder({ model: "${embedModel}" });
244
+ const rag = createRAGPipeline({ embedder, vectorStore });
245
+
246
+ export async function POST(c: Context) {
247
+ const { text, idPrefix } = await c.req.json();
248
+ if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
249
+ const result = await rag.ingest(text, idPrefix ?? "doc");
250
+ return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
251
+ }
252
+ `
253
+ );
254
+ }
255
+ if (auth === "better-auth") {
256
+ fs.mkdirSync(path.join(targetDir, "src", "routes", "api", "auth"), { recursive: true });
257
+ fs.writeFileSync(
258
+ path.join(targetDir, "src", "routes", "api", "auth", "[...path].ts"),
259
+ `// ALL /api/auth/* \u2014 Better Auth handler
260
+ import type { Context } from "@voltx/server";
261
+ import { auth } from "../../../lib/auth";
262
+ import { createAuthHandler } from "@voltx/auth";
263
+
264
+ const handler = createAuthHandler(auth);
265
+
266
+ export const GET = (c: Context) => handler(c);
267
+ export const POST = (c: Context) => handler(c);
268
+ `
269
+ );
270
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
271
+ fs.writeFileSync(
272
+ path.join(targetDir, "src", "lib", "auth.ts"),
273
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
274
+
275
+ export const auth = createAuth("better-auth", {
276
+ database: process.env.DATABASE_URL!,
277
+ emailAndPassword: true,
278
+ });
279
+
280
+ export const authMiddleware = createAuthMiddleware({
281
+ provider: auth,
282
+ publicPaths: ["/api/auth", "/api/health", "/"],
283
+ });
284
+ `
285
+ );
286
+ } else if (auth === "jwt") {
287
+ fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
288
+ fs.writeFileSync(
289
+ path.join(targetDir, "src", "lib", "auth.ts"),
290
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
291
+
292
+ export const jwt = createAuth("jwt", {
293
+ secret: process.env.JWT_SECRET!,
294
+ expiresIn: "7d",
295
+ });
296
+
297
+ export const authMiddleware = createAuthMiddleware({
298
+ provider: jwt,
299
+ publicPaths: ["/api/auth", "/api/health", "/"],
300
+ });
301
+ `
302
+ );
303
+ fs.writeFileSync(
304
+ path.join(targetDir, "src", "routes", "api", "auth.ts"),
305
+ `import type { Context } from "@voltx/server";
306
+ import { jwt } from "../../lib/auth";
307
+
308
+ export async function POST(c: Context) {
309
+ const { email, password } = await c.req.json();
310
+ if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
311
+ const token = await jwt.sign({ sub: email, email });
312
+ return c.json({ token });
313
+ }
314
+ `
315
+ );
316
+ }
317
+ let envContent = "";
318
+ if (template === "rag-app") {
319
+ envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nOPENAI_API_KEY=sk-...\n\n";
320
+ envContent += "# \u2500\u2500\u2500 Database \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
321
+ } else if (template === "chatbot" || template === "agent-app") {
322
+ envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nCEREBRAS_API_KEY=csk-...\n\n";
323
+ if (template === "agent-app") {
324
+ envContent += "# \u2500\u2500\u2500 Database (optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
325
+ envContent += "# \u2500\u2500\u2500 Tool API Keys (add keys for tools you use) \u2500\u2500\n";
326
+ envContent += "# TAVILY_API_KEY=tvly-... (Web Search \u2014 https://tavily.com)\n";
327
+ envContent += "# SERPER_API_KEY= (Google Search \u2014 https://serper.dev)\n";
328
+ envContent += "# OPENWEATHER_API_KEY= (Weather \u2014 https://openweathermap.org/api)\n";
329
+ envContent += "# NEWS_API_KEY= (News \u2014 https://newsapi.org)\n\n";
330
+ }
331
+ } else {
332
+ envContent += "# \u2500\u2500\u2500 LLM Provider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# OPENAI_API_KEY=sk-...\n# CEREBRAS_API_KEY=csk-...\n\n";
333
+ }
334
+ if (auth === "better-auth") {
335
+ envContent += "# \u2500\u2500\u2500 Auth (Better Auth) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nBETTER_AUTH_SECRET=your-secret-key-min-32-chars-here\nBETTER_AUTH_URL=http://localhost:3000\n\n";
336
+ } else if (auth === "jwt") {
337
+ envContent += "# \u2500\u2500\u2500 Auth (JWT) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nJWT_SECRET=your-jwt-secret-key\n\n";
338
+ }
339
+ envContent += "# \u2500\u2500\u2500 App \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPORT=3000\nNODE_ENV=development\n";
340
+ fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
341
+ if (template !== "blank") {
342
+ fs.writeFileSync(path.join(targetDir, "public", "index.html"), generateFrontendHTML(name, template));
343
+ }
344
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n");
345
+ printWelcomeBanner(name);
346
+ }
347
+ function generateFrontendHTML(projectName, template) {
348
+ const badge = template === "chatbot" ? "Chatbot" : template === "rag-app" ? "RAG App" : "Agent App";
349
+ const accentClass = template === "rag-app" ? "emerald" : template === "agent-app" ? "purple" : "blue";
350
+ const shell = (body) => `<!DOCTYPE html>
351
+ <html lang="en" class="h-full">
352
+ <head>
353
+ <meta charset="UTF-8" />
354
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
355
+ <title>${projectName}</title>
356
+ <script src="https://cdn.tailwindcss.com"></script>
357
+ <style>
358
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
359
+ body { font-family: 'Inter', system-ui, sans-serif; }
360
+ .msg-enter { animation: msgIn 0.25s ease-out; }
361
+ @keyframes msgIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
362
+ .typing-dot { animation: blink 1.4s infinite both; }
363
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
364
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
365
+ @keyframes blink { 0%, 80%, 100% { opacity: 0.2; } 40% { opacity: 1; } }
366
+ ::-webkit-scrollbar { width: 6px; }
367
+ ::-webkit-scrollbar-track { background: transparent; }
368
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
369
+ </style>
370
+ </head>
371
+ <body class="h-full bg-gray-950 text-gray-100">
372
+ <div id="app" class="h-full flex flex-col">
373
+ <header class="flex-shrink-0 border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm px-4 py-3">
374
+ <div class="max-w-4xl mx-auto flex items-center justify-between">
375
+ <div class="flex items-center gap-3">
376
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center text-sm font-bold">V</div>
377
+ <h1 class="text-lg font-semibold text-white">${projectName}</h1>
378
+ <span class="text-xs px-2 py-0.5 rounded-full bg-gray-800 text-gray-400">${badge}</span>
379
+ </div>
380
+ <a href="https://github.com/codewithshail/voltx" target="_blank" class="text-gray-500 hover:text-gray-300 transition-colors text-sm">Built with VoltX</a>
381
+ </div>
382
+ </header>
383
+ <main class="flex-1 overflow-hidden"><div class="h-full max-w-4xl mx-auto">
384
+ ${body}
385
+ </div></main>
386
+ </div>
387
+ </body>
388
+ </html>`;
389
+ if (template === "chatbot") return shell(chatbotBody());
390
+ if (template === "rag-app") return shell(ragAppBody());
391
+ if (template === "agent-app") return shell(agentAppBody());
392
+ return "";
393
+ }
394
+ function chatbotBody() {
395
+ return ` <div class="h-full flex flex-col">
396
+ <div id="messages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
397
+ <div class="text-center py-12">
398
+ <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-blue-500/20 to-purple-600/20 border border-blue-500/20 flex items-center justify-center">
399
+ <svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/></svg>
400
+ </div>
401
+ <h2 class="text-xl font-semibold text-white mb-2">Start a conversation</h2>
402
+ <p class="text-gray-500 text-sm">Type a message below to chat with your AI assistant.</p>
403
+ </div>
404
+ </div>
405
+ <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
406
+ <form id="chatForm" class="flex gap-3">
407
+ <input id="chatInput" type="text" placeholder="Type your message..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors" />
408
+ <button type="submit" id="sendBtn" class="px-5 py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Send</button>
409
+ </form>
410
+ </div>
411
+ </div>
412
+ <script>
413
+ const messages=[], messagesEl=document.getElementById("messages"), form=document.getElementById("chatForm"), input=document.getElementById("chatInput"), sendBtn=document.getElementById("sendBtn");
414
+ function addMsg(role,content){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-blue-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed";b.textContent=content;d.appendChild(b);const w=messagesEl.querySelector(".text-center.py-12");if(w)w.remove();messagesEl.appendChild(d);messagesEl.scrollTop=messagesEl.scrollHeight;return b}
415
+ form.addEventListener("submit",async e=>{e.preventDefault();const text=input.value.trim();if(!text)return;messages.push({role:"user",content:text});addMsg("user",text);input.value="";sendBtn.disabled=true;try{const res=await fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({messages,conversationId:"default"})});if(!res.ok){addMsg("assistant","Error: "+res.status);sendBtn.disabled=false;return}const bubble=addMsg("assistant","");const reader=res.body.getReader();const dec=new TextDecoder();let full="";while(true){const{done,value}=await reader.read();if(done)break;const chunk=dec.decode(value,{stream:true});for(const line of chunk.split("\\n")){if(line.startsWith("data: ")){const d=line.slice(6);if(d==="[DONE]")continue;try{const p=JSON.parse(d);const t=p.choices?.[0]?.delta?.content||p.content||p.text||"";full+=t;bubble.textContent=full;messagesEl.scrollTop=messagesEl.scrollHeight}catch{}}}}messages.push({role:"assistant",content:full})}catch(err){addMsg("assistant","Error: "+err.message)}sendBtn.disabled=false;input.focus()});
416
+ input.focus();
417
+ </script>`;
418
+ }
419
+ function ragAppBody() {
420
+ return ` <div class="h-full flex flex-col md:flex-row">
421
+ <div class="md:w-80 flex-shrink-0 border-b md:border-b-0 md:border-r border-gray-800 flex flex-col">
422
+ <div class="px-4 py-3 border-b border-gray-800"><h2 class="text-sm font-semibold text-white flex items-center gap-2"><svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/></svg>Ingest Documents</h2></div>
423
+ <div class="flex-1 p-4 flex flex-col gap-3">
424
+ <textarea id="ingestText" rows="6" placeholder="Paste text to ingest..." class="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-emerald-500 resize-none"></textarea>
425
+ <button id="ingestBtn" onclick="ingestDoc()" class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-lg transition-colors">Ingest</button>
426
+ <div id="ingestStatus" class="text-xs text-gray-500 hidden"></div>
427
+ <div class="mt-auto pt-3 border-t border-gray-800"><p class="text-xs text-gray-600">Documents are chunked, embedded, and stored for retrieval.</p></div>
428
+ </div>
429
+ </div>
430
+ <div class="flex-1 flex flex-col min-w-0">
431
+ <div id="ragMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
432
+ <div class="text-center py-12">
433
+ <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-emerald-500/20 to-blue-600/20 border border-emerald-500/20 flex items-center justify-center"><svg class="w-8 h-8 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg></div>
434
+ <h2 class="text-xl font-semibold text-white mb-2">Ask your documents</h2>
435
+ <p class="text-gray-500 text-sm">Ingest documents on the left, then ask questions here.</p>
436
+ </div>
437
+ </div>
438
+ <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
439
+ <form id="ragForm" class="flex gap-3">
440
+ <input id="ragInput" type="text" placeholder="Ask about your documents..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500 transition-colors" />
441
+ <button type="submit" id="ragSendBtn" class="px-5 py-3 bg-emerald-600 hover:bg-emerald-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Ask</button>
442
+ </form>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ <script>
447
+ const ragEl=document.getElementById("ragMessages");
448
+ function addRagMsg(role,c){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-emerald-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed";b.textContent=c;d.appendChild(b);const w=ragEl.querySelector(".text-center.py-12");if(w)w.remove();ragEl.appendChild(d);ragEl.scrollTop=ragEl.scrollHeight;return b}
449
+ async function ingestDoc(){const text=document.getElementById("ingestText").value.trim();if(!text)return;const btn=document.getElementById("ingestBtn"),st=document.getElementById("ingestStatus");btn.disabled=true;btn.textContent="Ingesting...";st.className="text-xs text-yellow-400";st.textContent="Processing...";st.classList.remove("hidden");try{const res=await fetch("/api/rag/ingest",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text})});const data=await res.json();if(res.ok){st.className="text-xs text-emerald-400";st.textContent="Ingested "+(data.chunks||0)+" chunks.";document.getElementById("ingestText").value=""}else{st.className="text-xs text-red-400";st.textContent="Error: "+(data.error||res.statusText)}}catch(e){st.className="text-xs text-red-400";st.textContent="Error: "+e.message}btn.disabled=false;btn.textContent="Ingest"}
450
+ document.getElementById("ragForm").addEventListener("submit",async e=>{e.preventDefault();const input=document.getElementById("ragInput"),text=input.value.trim();if(!text)return;addRagMsg("user",text);input.value="";document.getElementById("ragSendBtn").disabled=true;try{const res=await fetch("/api/rag/query",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({question:text})});if(!res.ok){addRagMsg("assistant","Error: "+res.status);document.getElementById("ragSendBtn").disabled=false;return}const bubble=addRagMsg("assistant","");const reader=res.body.getReader();const dec=new TextDecoder();let full="";while(true){const{done,value}=await reader.read();if(done)break;const chunk=dec.decode(value,{stream:true});for(const line of chunk.split("\\n")){if(line.startsWith("data: ")){const d=line.slice(6);if(d==="[DONE]")continue;try{const p=JSON.parse(d);const t=p.choices?.[0]?.delta?.content||p.content||p.text||"";full+=t;bubble.textContent=full;ragEl.scrollTop=ragEl.scrollHeight}catch{}}}};}catch(err){addRagMsg("assistant","Error: "+err.message)}document.getElementById("ragSendBtn").disabled=false;document.getElementById("ragInput").focus()});
451
+ document.getElementById("ragInput").focus();
452
+ </script>`;
453
+ }
454
+ function agentAppBody() {
455
+ return ` <div class="h-full flex flex-col">
456
+ <div id="agentMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
457
+ <div class="text-center py-12">
458
+ <div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-purple-500/20 to-orange-500/20 border border-purple-500/20 flex items-center justify-center"><svg class="w-8 h-8 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg></div>
459
+ <h2 class="text-xl font-semibold text-white mb-2">AI Agent</h2>
460
+ <p class="text-gray-500 text-sm">Your agent can use tools to answer questions accurately.</p>
461
+ </div>
462
+ </div>
463
+ <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
464
+ <form id="agentForm" class="flex gap-3">
465
+ <input id="agentInput" type="text" placeholder="Ask the agent anything..." autocomplete="off" class="flex-1 bg-gray-900 border border-gray-700 rounded-xl px-4 py-3 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition-colors" />
466
+ <button type="submit" id="agentSendBtn" class="px-5 py-3 bg-purple-600 hover:bg-purple-500 disabled:bg-gray-700 disabled:text-gray-500 text-white text-sm font-medium rounded-xl transition-colors">Run</button>
467
+ </form>
468
+ </div>
469
+ </div>
470
+ <script>
471
+ const agentEl=document.getElementById("agentMessages");
472
+ function addAgentMsg(role,c,isStep){const d=document.createElement("div");d.className="msg-enter flex "+(role==="user"?"justify-end":"justify-start");const b=document.createElement("div");if(isStep){b.className="max-w-[85%] px-3 py-2 rounded-lg bg-gray-900 border border-gray-700 text-xs text-gray-400 font-mono"}else{b.className=role==="user"?"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-br-md bg-purple-600 text-white text-sm leading-relaxed":"max-w-[75%] px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 text-gray-200 text-sm leading-relaxed whitespace-pre-wrap"}b.textContent=c;d.appendChild(b);const w=agentEl.querySelector(".text-center.py-12");if(w)w.remove();agentEl.appendChild(d);agentEl.scrollTop=agentEl.scrollHeight;return b}
473
+ function addThinking(){const d=document.createElement("div");d.id="thinking";d.className="msg-enter flex justify-start";d.innerHTML='<div class="px-4 py-2.5 rounded-2xl rounded-bl-md bg-gray-800 flex items-center gap-2 text-sm text-gray-400"><svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path></svg>Agent is thinking...</div>';agentEl.appendChild(d);agentEl.scrollTop=agentEl.scrollHeight}
474
+ function removeThinking(){const t=document.getElementById("thinking");if(t)t.remove()}
475
+ document.getElementById("agentForm").addEventListener("submit",async e=>{e.preventDefault();const input=document.getElementById("agentInput"),text=input.value.trim();if(!text)return;addAgentMsg("user",text);input.value="";document.getElementById("agentSendBtn").disabled=true;addThinking();try{const res=await fetch("/api/agent",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({input:text})});removeThinking();if(!res.ok){const err=await res.json().catch(()=>({}));addAgentMsg("assistant","Error: "+(err.error||res.statusText));document.getElementById("agentSendBtn").disabled=false;return}const data=await res.json();if(data.steps&&data.steps.length>0){for(const s of data.steps){const name=s.tool||s.name||"tool";const inp=typeof s.input==="string"?s.input:JSON.stringify(s.input||{});const out=typeof s.output==="string"?s.output:JSON.stringify(s.output||{});addAgentMsg("assistant","\u{1F527} "+name+"("+inp+")\\n\u2192 "+out.slice(0,300),true)}}addAgentMsg("assistant",data.content||"No response.")}catch(err){removeThinking();addAgentMsg("assistant","Error: "+err.message)}document.getElementById("agentSendBtn").disabled=false;input.focus()});
476
+ document.getElementById("agentInput").focus();
477
+ </script>`;
478
+ }
479
+ if (typeof __require !== "undefined" && __require.main === module && process.argv[1]?.includes("create")) {
480
+ const projectName = process.argv[2];
481
+ if (!projectName) {
482
+ console.log("Usage: create-voltx-app <project-name> [--template chatbot] [--auth jwt]");
483
+ process.exit(1);
484
+ }
485
+ const templateFlag = process.argv.indexOf("--template");
486
+ const template = templateFlag !== -1 ? process.argv[templateFlag + 1] : "blank";
487
+ const authFlag = process.argv.indexOf("--auth");
488
+ const auth = authFlag !== -1 ? process.argv[authFlag + 1] : "none";
489
+ createProject({ name: projectName, template, auth }).catch((err) => {
490
+ console.error("[voltx] Error:", err);
491
+ process.exit(1);
492
+ });
493
+ }
494
+
495
+ export {
496
+ createProject
497
+ };