@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.
package/dist/create.js CHANGED
@@ -142,6 +142,13 @@ function printWelcomeBanner(projectName) {
142
142
  }
143
143
 
144
144
  // src/create.ts
145
+ var V = "^0.3.0";
146
+ var TEMPLATE_DEPS = {
147
+ blank: { "@voltx/core": V, "@voltx/server": V },
148
+ chatbot: { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/memory": V },
149
+ "rag-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/rag": V, "@voltx/db": V },
150
+ "agent-app": { "@voltx/core": V, "@voltx/ai": V, "@voltx/server": V, "@voltx/agents": V, "@voltx/memory": V }
151
+ };
145
152
  async function createProject(options) {
146
153
  const { name, template = "blank", auth = "none" } = options;
147
154
  const targetDir = path.resolve(process.cwd(), name);
@@ -150,62 +157,26 @@ async function createProject(options) {
150
157
  process.exit(1);
151
158
  }
152
159
  fs.mkdirSync(targetDir, { recursive: true });
153
- const templateDeps = {
154
- blank: {
155
- "@voltx/core": "^0.3.0",
156
- "@voltx/server": "^0.3.0"
157
- },
158
- chatbot: {
159
- "@voltx/core": "^0.3.0",
160
- "@voltx/ai": "^0.3.0",
161
- "@voltx/server": "^0.3.0",
162
- "@voltx/memory": "^0.3.0"
163
- },
164
- "rag-app": {
165
- "@voltx/core": "^0.3.0",
166
- "@voltx/ai": "^0.3.0",
167
- "@voltx/server": "^0.3.0",
168
- "@voltx/rag": "^0.3.0",
169
- "@voltx/db": "^0.3.0"
170
- },
171
- "agent-app": {
172
- "@voltx/core": "^0.3.0",
173
- "@voltx/ai": "^0.3.0",
174
- "@voltx/server": "^0.3.0",
175
- "@voltx/agents": "^0.3.0",
176
- "@voltx/memory": "^0.3.0"
177
- }
178
- };
179
- const packageJson = {
160
+ const provider = template === "rag-app" ? "openai" : "cerebras";
161
+ const model = template === "rag-app" ? "gpt-4o" : "llama3.1-8b";
162
+ const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
163
+ const deps = { ...TEMPLATE_DEPS[template] ?? TEMPLATE_DEPS["blank"], "@voltx/cli": V };
164
+ if (auth === "better-auth") {
165
+ deps["@voltx/auth"] = V;
166
+ deps["better-auth"] = "^1.5.0";
167
+ } else if (auth === "jwt") {
168
+ deps["@voltx/auth"] = V;
169
+ deps["jose"] = "^6.0.0";
170
+ }
171
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify({
180
172
  name,
181
173
  version: "0.1.0",
182
174
  private: true,
183
- scripts: {
184
- dev: "voltx dev",
185
- build: "voltx build",
186
- start: "voltx start"
187
- },
188
- dependencies: {
189
- ...templateDeps[template] ?? templateDeps["blank"],
190
- "@voltx/cli": "^0.3.0",
191
- ...auth === "better-auth" ? { "@voltx/auth": "^0.3.0", "better-auth": "^1.5.0" } : {},
192
- ...auth === "jwt" ? { "@voltx/auth": "^0.3.0", "jose": "^6.0.0" } : {}
193
- },
194
- devDependencies: {
195
- typescript: "^5.7.0",
196
- tsx: "^4.21.0",
197
- tsup: "^8.0.0",
198
- "@types/node": "^22.0.0"
199
- }
200
- };
201
- fs.writeFileSync(
202
- path.join(targetDir, "package.json"),
203
- JSON.stringify(packageJson, null, 2)
204
- );
205
- const hasDb = template === "rag-app" || template === "agent-app" || auth === "better-auth";
206
- const provider = template === "rag-app" ? "openai" : "cerebras";
207
- const model = template === "rag-app" ? "gpt-4o" : "llama-4-scout-17b-16e";
208
- let configContent = `import { defineConfig } from "@voltx/core";
175
+ scripts: { dev: "voltx dev", build: "voltx build", start: "voltx start" },
176
+ dependencies: deps,
177
+ devDependencies: { typescript: "^5.7.0", tsx: "^4.21.0", tsup: "^8.0.0", "@types/node": "^22.0.0" }
178
+ }, null, 2));
179
+ let config = `import { defineConfig } from "@voltx/core";
209
180
 
210
181
  export default defineConfig({
211
182
  name: "${name}",
@@ -214,19 +185,15 @@ export default defineConfig({
214
185
  provider: "${provider}",
215
186
  model: "${model}",
216
187
  },`;
217
- if (hasDb) {
218
- configContent += `
188
+ if (hasDb) config += `
219
189
  db: {
220
190
  url: process.env.DATABASE_URL,
221
191
  },`;
222
- }
223
- if (auth !== "none") {
224
- configContent += `
192
+ if (auth !== "none") config += `
225
193
  auth: {
226
194
  provider: "${auth}",
227
195
  },`;
228
- }
229
- configContent += `
196
+ config += `
230
197
  server: {
231
198
  routesDir: "src/routes",
232
199
  staticDir: "public",
@@ -234,9 +201,13 @@ export default defineConfig({
234
201
  },
235
202
  });
236
203
  `;
237
- fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), configContent);
204
+ fs.writeFileSync(path.join(targetDir, "voltx.config.ts"), config);
238
205
  fs.mkdirSync(path.join(targetDir, "src", "routes", "api"), { recursive: true });
239
206
  fs.mkdirSync(path.join(targetDir, "public"), { recursive: true });
207
+ fs.writeFileSync(path.join(targetDir, "tsconfig.json"), JSON.stringify({
208
+ compilerOptions: { target: "ES2022", module: "ESNext", moduleResolution: "bundler", strict: true, esModuleInterop: true, skipLibCheck: true, outDir: "dist" },
209
+ include: ["src", "voltx.config.ts"]
210
+ }, null, 2));
240
211
  fs.writeFileSync(
241
212
  path.join(targetDir, "src", "index.ts"),
242
213
  `import { createApp } from "@voltx/core";
@@ -264,19 +235,16 @@ import type { Context } from "@voltx/server";
264
235
  import { streamText } from "@voltx/ai";
265
236
  import { createMemory } from "@voltx/memory";
266
237
 
267
- // In-memory for dev; swap to createMemory("postgres", { url }) for production
268
238
  const memory = createMemory({ maxMessages: 50 });
269
239
 
270
240
  export async function POST(c: Context) {
271
241
  const { messages, conversationId = "default" } = await c.req.json();
272
242
 
273
- // Store the latest user message
274
243
  const lastMessage = messages[messages.length - 1];
275
244
  if (lastMessage?.role === "user") {
276
245
  await memory.add(conversationId, { role: "user", content: lastMessage.content });
277
246
  }
278
247
 
279
- // Get conversation history from memory
280
248
  const history = await memory.get(conversationId);
281
249
 
282
250
  const result = await streamText({
@@ -285,7 +253,6 @@ export async function POST(c: Context) {
285
253
  messages: history.map((m) => ({ role: m.role, content: m.content })),
286
254
  });
287
255
 
288
- // Store assistant response after stream completes
289
256
  result.text.then(async (text) => {
290
257
  await memory.add(conversationId, { role: "assistant", content: text });
291
258
  });
@@ -298,41 +265,69 @@ export async function POST(c: Context) {
298
265
  if (template === "agent-app") {
299
266
  fs.mkdirSync(path.join(targetDir, "src", "agents"), { recursive: true });
300
267
  fs.mkdirSync(path.join(targetDir, "src", "tools"), { recursive: true });
301
- fs.writeFileSync(
302
- path.join(targetDir, "src", "agents", "assistant.ts"),
303
- `import { createAgent } from "@voltx/agents";
304
- import { searchTool } from "../tools/search";
268
+ fs.writeFileSync(path.join(targetDir, "src", "tools", "calculator.ts"), `// Calculator tool \u2014 evaluates math expressions (no API key needed)
269
+ import type { Tool } from "@voltx/agents";
305
270
 
306
- export const assistant = createAgent({
307
- name: "assistant",
308
- model: "${provider}:${model}",
309
- instructions: "You are a helpful AI assistant. Use your tools when needed.",
310
- tools: [searchTool],
311
- maxIterations: 5,
312
- });
313
- `
314
- );
315
- fs.writeFileSync(
316
- path.join(targetDir, "src", "tools", "search.ts"),
317
- `import type { Tool } from "@voltx/agents";
271
+ export const calculatorTool: Tool = {
272
+ name: "calculator",
273
+ description: "Evaluate a math expression. Supports +, -, *, /, %, parentheses, and Math functions.",
274
+ parameters: {
275
+ type: "object",
276
+ properties: { expression: { type: "string", description: "The math expression to evaluate" } },
277
+ required: ["expression"],
278
+ },
279
+ async execute(args: { expression: string }) {
280
+ try {
281
+ const safe = args.expression.replace(/[^0-9+\\-*/.()%\\s,]|(?<!Math)\\.[a-z]/gi, (match) => {
282
+ if (args.expression.includes("Math.")) return match;
283
+ throw new Error("Invalid character: " + match);
284
+ });
285
+ const result = new Function("return " + safe)();
286
+ return \`\${args.expression} = \${result}\`;
287
+ } catch (err) {
288
+ return \`Error: \${err instanceof Error ? err.message : String(err)}\`;
289
+ }
290
+ },
291
+ };
292
+ `);
293
+ fs.writeFileSync(path.join(targetDir, "src", "tools", "datetime.ts"), `// Date & time tool \u2014 returns current date, time, timezone (no API key needed)
294
+ import type { Tool } from "@voltx/agents";
318
295
 
319
- export const searchTool: Tool = {
320
- name: "search",
321
- description: "Search for information on a topic.",
296
+ export const datetimeTool: Tool = {
297
+ name: "datetime",
298
+ description: "Get the current date, time, day of week, and timezone.",
322
299
  parameters: {
323
300
  type: "object",
324
- properties: { query: { type: "string", description: "The search query" } },
325
- required: ["query"],
301
+ properties: { timezone: { type: "string", description: "Optional IANA timezone. Defaults to server timezone." } },
326
302
  },
327
- async execute(args: { query: string }) {
328
- return \`Search results for "\${args.query}": Placeholder \u2014 connect a real search API.\`;
303
+ async execute(args: { timezone?: string }) {
304
+ const now = new Date();
305
+ const tz = args.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
306
+ const formatted = now.toLocaleString("en-US", {
307
+ timeZone: tz, weekday: "long", year: "numeric", month: "long", day: "numeric",
308
+ hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true,
309
+ });
310
+ return \`Current date/time (\${tz}): \${formatted}\`;
329
311
  },
330
312
  };
331
- `
332
- );
313
+ `);
314
+ fs.writeFileSync(path.join(targetDir, "src", "agents", "assistant.ts"), `// AI Agent \u2014 autonomous assistant with tools
315
+ import { createAgent } from "@voltx/agents";
316
+ import { calculatorTool } from "../tools/calculator";
317
+ import { datetimeTool } from "../tools/datetime";
318
+
319
+ export const assistant = createAgent({
320
+ name: "assistant",
321
+ model: "${provider}:${model}",
322
+ instructions: "You are a helpful AI assistant with access to tools: Calculator, Date & Time. Use them when needed to answer questions accurately.",
323
+ tools: [calculatorTool, datetimeTool],
324
+ maxIterations: 5,
325
+ });
326
+ `);
333
327
  fs.writeFileSync(
334
328
  path.join(targetDir, "src", "routes", "api", "agent.ts"),
335
- `import type { Context } from "@voltx/server";
329
+ `// POST /api/agent \u2014 Run the AI agent
330
+ import type { Context } from "@voltx/server";
336
331
  import { assistant } from "../../agents/assistant";
337
332
 
338
333
  export async function POST(c: Context) {
@@ -355,21 +350,18 @@ import { streamText } from "@voltx/ai";
355
350
  import { createRAGPipeline, createEmbedder } from "@voltx/rag";
356
351
  import { createVectorStore } from "@voltx/db";
357
352
 
358
- const vectorStore = createVectorStore(); // swap to "pinecone" or "pgvector" for production
353
+ const vectorStore = createVectorStore();
359
354
  const embedder = createEmbedder({ model: "${embedModel}" });
360
355
  const rag = createRAGPipeline({ embedder, vectorStore });
361
356
 
362
357
  export async function POST(c: Context) {
363
358
  const { question } = await c.req.json();
364
-
365
359
  const context = await rag.getContext(question, { topK: 5 });
366
-
367
360
  const result = await streamText({
368
361
  model: "${provider}:${model}",
369
- system: \`Answer the user's question based on the following context. If the context doesn't contain relevant information, say so.\\n\\nContext:\\n\${context}\`,
362
+ system: \`Answer based on context. If not relevant, say so.\\n\\nContext:\\n\${context}\`,
370
363
  messages: [{ role: "user", content: question }],
371
364
  });
372
-
373
365
  return result.toSSEResponse();
374
366
  }
375
367
  `
@@ -387,11 +379,7 @@ const rag = createRAGPipeline({ embedder, vectorStore });
387
379
 
388
380
  export async function POST(c: Context) {
389
381
  const { text, idPrefix } = await c.req.json();
390
-
391
- if (!text || typeof text !== "string") {
392
- return c.json({ error: "Missing 'text' field" }, 400);
393
- }
394
-
382
+ if (!text || typeof text !== "string") return c.json({ error: "Missing 'text' field" }, 400);
395
383
  const result = await rag.ingest(text, idPrefix ?? "doc");
396
384
  return c.json({ status: "ok", chunks: result.chunks, ids: result.ids });
397
385
  }
@@ -416,8 +404,7 @@ export const POST = (c: Context) => handler(c);
416
404
  fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
417
405
  fs.writeFileSync(
418
406
  path.join(targetDir, "src", "lib", "auth.ts"),
419
- `// Auth configuration \u2014 Better Auth with DB-backed sessions
420
- import { createAuth, createAuthMiddleware } from "@voltx/auth";
407
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
421
408
 
422
409
  export const auth = createAuth("better-auth", {
423
410
  database: process.env.DATABASE_URL!,
@@ -434,8 +421,7 @@ export const authMiddleware = createAuthMiddleware({
434
421
  fs.mkdirSync(path.join(targetDir, "src", "lib"), { recursive: true });
435
422
  fs.writeFileSync(
436
423
  path.join(targetDir, "src", "lib", "auth.ts"),
437
- `// Auth configuration \u2014 JWT (stateless)
438
- import { createAuth, createAuthMiddleware } from "@voltx/auth";
424
+ `import { createAuth, createAuthMiddleware } from "@voltx/auth";
439
425
 
440
426
  export const jwt = createAuth("jwt", {
441
427
  secret: process.env.JWT_SECRET!,
@@ -450,17 +436,12 @@ export const authMiddleware = createAuthMiddleware({
450
436
  );
451
437
  fs.writeFileSync(
452
438
  path.join(targetDir, "src", "routes", "api", "auth.ts"),
453
- `// POST /api/auth/login \u2014 Example JWT login route
454
- import type { Context } from "@voltx/server";
439
+ `import type { Context } from "@voltx/server";
455
440
  import { jwt } from "../../lib/auth";
456
441
 
457
442
  export async function POST(c: Context) {
458
443
  const { email, password } = await c.req.json();
459
-
460
- if (!email || !password) {
461
- return c.json({ error: "Email and password are required" }, 400);
462
- }
463
-
444
+ if (!email || !password) return c.json({ error: "Email and password are required" }, 400);
464
445
  const token = await jwt.sign({ sub: email, email });
465
446
  return c.json({ token });
466
447
  }
@@ -470,55 +451,166 @@ export async function POST(c: Context) {
470
451
  let envContent = "";
471
452
  if (template === "rag-app") {
472
453
  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";
473
- envContent += "# \u2500\u2500\u2500 Database (Neon Postgres) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require\n\n";
474
- envContent += "# \u2500\u2500\u2500 Vector Database (Pinecone) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nPINECONE_API_KEY=pc-...\nPINECONE_INDEX=voltx-embeddings\n\n";
454
+ 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";
475
455
  } else if (template === "chatbot" || template === "agent-app") {
476
456
  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";
477
457
  if (template === "agent-app") {
478
- envContent += "# \u2500\u2500\u2500 Database (Neon Postgres \u2014 optional) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nDATABASE_URL=\n\n";
458
+ 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";
459
+ envContent += "# \u2500\u2500\u2500 Tool API Keys (add keys for tools you use) \u2500\u2500\n";
460
+ envContent += "# TAVILY_API_KEY=tvly-... (Web Search \u2014 https://tavily.com)\n";
461
+ envContent += "# SERPER_API_KEY= (Google Search \u2014 https://serper.dev)\n";
462
+ envContent += "# OPENWEATHER_API_KEY= (Weather \u2014 https://openweathermap.org/api)\n";
463
+ envContent += "# NEWS_API_KEY= (News \u2014 https://newsapi.org)\n\n";
479
464
  }
480
465
  } else {
481
- envContent += "# \u2500\u2500\u2500 LLM Provider (add your key) \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";
466
+ 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";
482
467
  }
483
468
  if (auth === "better-auth") {
484
- 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";
485
- if (template !== "rag-app" && template !== "agent-app") {
486
- envContent += "DATABASE_URL=postgresql://user:pass@ep-xxx.us-east-2.aws.neon.tech/dbname?sslmode=require\n";
487
- }
488
- envContent += "# GITHUB_CLIENT_ID=\n# GITHUB_CLIENT_SECRET=\n\n";
469
+ 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";
489
470
  } else if (auth === "jwt") {
490
471
  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";
491
472
  }
492
473
  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";
493
474
  fs.writeFileSync(path.join(targetDir, ".env.example"), envContent);
494
- fs.writeFileSync(
495
- path.join(targetDir, ".gitignore"),
496
- "node_modules\ndist\n.env\n"
497
- );
498
- fs.writeFileSync(
499
- path.join(targetDir, "tsconfig.json"),
500
- JSON.stringify(
501
- {
502
- compilerOptions: {
503
- target: "ES2022",
504
- module: "ESNext",
505
- moduleResolution: "bundler",
506
- strict: true,
507
- esModuleInterop: true,
508
- skipLibCheck: true,
509
- outDir: "dist",
510
- rootDir: "src"
511
- },
512
- include: ["src"]
513
- },
514
- null,
515
- 2
516
- )
517
- );
475
+ if (template !== "blank") {
476
+ fs.writeFileSync(path.join(targetDir, "public", "index.html"), generateFrontendHTML(name, template));
477
+ }
478
+ fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\ndist\n.env\n");
518
479
  printWelcomeBanner(name);
519
480
  }
520
- var isDirectRun = typeof require !== "undefined" && require.main === module && process.argv[1]?.includes("create");
521
- if (isDirectRun) {
481
+ function generateFrontendHTML(projectName, template) {
482
+ const badge = template === "chatbot" ? "Chatbot" : template === "rag-app" ? "RAG App" : "Agent App";
483
+ const accentClass = template === "rag-app" ? "emerald" : template === "agent-app" ? "purple" : "blue";
484
+ const shell = (body) => `<!DOCTYPE html>
485
+ <html lang="en" class="h-full">
486
+ <head>
487
+ <meta charset="UTF-8" />
488
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
489
+ <title>${projectName}</title>
490
+ <script src="https://cdn.tailwindcss.com/3.4.17"></script>
491
+ <style>
492
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
493
+ body { font-family: 'Inter', system-ui, sans-serif; }
494
+ .msg-enter { animation: msgIn 0.25s ease-out; }
495
+ @keyframes msgIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
496
+ .typing-dot { animation: blink 1.4s infinite both; }
497
+ .typing-dot:nth-child(2) { animation-delay: 0.2s; }
498
+ .typing-dot:nth-child(3) { animation-delay: 0.4s; }
499
+ @keyframes blink { 0%, 80%, 100% { opacity: 0.2; } 40% { opacity: 1; } }
500
+ ::-webkit-scrollbar { width: 6px; }
501
+ ::-webkit-scrollbar-track { background: transparent; }
502
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
503
+ </style>
504
+ </head>
505
+ <body class="h-full bg-gray-950 text-gray-100">
506
+ <div id="app" class="h-full flex flex-col">
507
+ <header class="flex-shrink-0 border-b border-gray-800 bg-gray-950/80 backdrop-blur-sm px-4 py-3">
508
+ <div class="max-w-4xl mx-auto flex items-center justify-between">
509
+ <div class="flex items-center gap-3">
510
+ <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>
511
+ <h1 class="text-lg font-semibold text-white">${projectName}</h1>
512
+ <span class="text-xs px-2 py-0.5 rounded-full bg-gray-800 text-gray-400">${badge}</span>
513
+ </div>
514
+ <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>
515
+ </div>
516
+ </header>
517
+ <main class="flex-1 overflow-hidden"><div class="h-full max-w-4xl mx-auto">
518
+ ${body}
519
+ </div></main>
520
+ </div>
521
+ </body>
522
+ </html>`;
523
+ if (template === "chatbot") return shell(chatbotBody());
524
+ if (template === "rag-app") return shell(ragAppBody());
525
+ if (template === "agent-app") return shell(agentAppBody());
526
+ return "";
527
+ }
528
+ function chatbotBody() {
529
+ return ` <div class="h-full flex flex-col">
530
+ <div id="messages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
531
+ <div class="text-center py-12">
532
+ <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">
533
+ <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>
534
+ </div>
535
+ <h2 class="text-xl font-semibold text-white mb-2">Start a conversation</h2>
536
+ <p class="text-gray-500 text-sm">Type a message below to chat with your AI assistant.</p>
537
+ </div>
538
+ </div>
539
+ <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
540
+ <form id="chatForm" class="flex gap-3">
541
+ <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" />
542
+ <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>
543
+ </form>
544
+ </div>
545
+ </div>
546
+ <script>
547
+ const messages=[], messagesEl=document.getElementById("messages"), form=document.getElementById("chatForm"), input=document.getElementById("chatInput"), sendBtn=document.getElementById("sendBtn");
548
+ 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}
549
+ 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.textDelta||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()});
550
+ input.focus();
551
+ </script>`;
552
+ }
553
+ function ragAppBody() {
554
+ return ` <div class="h-full flex flex-col md:flex-row">
555
+ <div class="md:w-80 flex-shrink-0 border-b md:border-b-0 md:border-r border-gray-800 flex flex-col">
556
+ <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>
557
+ <div class="flex-1 p-4 flex flex-col gap-3">
558
+ <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>
559
+ <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>
560
+ <div id="ingestStatus" class="text-xs text-gray-500 hidden"></div>
561
+ <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>
562
+ </div>
563
+ </div>
564
+ <div class="flex-1 flex flex-col min-w-0">
565
+ <div id="ragMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
566
+ <div class="text-center py-12">
567
+ <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>
568
+ <h2 class="text-xl font-semibold text-white mb-2">Ask your documents</h2>
569
+ <p class="text-gray-500 text-sm">Ingest documents on the left, then ask questions here.</p>
570
+ </div>
571
+ </div>
572
+ <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
573
+ <form id="ragForm" class="flex gap-3">
574
+ <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" />
575
+ <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>
576
+ </form>
577
+ </div>
578
+ </div>
579
+ </div>
580
+ <script>
581
+ const ragEl=document.getElementById("ragMessages");
582
+ 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}
583
+ 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"}
584
+ 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.textDelta||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()});
585
+ document.getElementById("ragInput").focus();
586
+ </script>`;
587
+ }
588
+ function agentAppBody() {
589
+ return ` <div class="h-full flex flex-col">
590
+ <div id="agentMessages" class="flex-1 overflow-y-auto px-4 py-6 space-y-4">
591
+ <div class="text-center py-12">
592
+ <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>
593
+ <h2 class="text-xl font-semibold text-white mb-2">AI Agent</h2>
594
+ <p class="text-gray-500 text-sm">Your agent can use tools to answer questions accurately.</p>
595
+ </div>
596
+ </div>
597
+ <div class="flex-shrink-0 border-t border-gray-800 px-4 py-4">
598
+ <form id="agentForm" class="flex gap-3">
599
+ <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" />
600
+ <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>
601
+ </form>
602
+ </div>
603
+ </div>
604
+ <script>
605
+ const agentEl=document.getElementById("agentMessages");
606
+ 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}
607
+ 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}
608
+ function removeThinking(){const t=document.getElementById("thinking");if(t)t.remove()}
609
+ 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()});
610
+ document.getElementById("agentInput").focus();
611
+ </script>`;
612
+ }
613
+ if (typeof require !== "undefined" && require.main === module && process.argv[1]?.includes("create")) {
522
614
  const projectName = process.argv[2];
523
615
  if (!projectName) {
524
616
  console.log("Usage: create-voltx-app <project-name> [--template chatbot] [--auth jwt]");
package/dist/create.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createProject
4
- } from "./chunk-KFHPTRKZ.mjs";
4
+ } from "./chunk-P5FSO2UH.mjs";
5
5
  import "./chunk-IV352HZA.mjs";
6
6
  import "./chunk-Y6FXYEAI.mjs";
7
7
  export {
package/dist/generate.js CHANGED
@@ -78,7 +78,7 @@ import { createAgent } from "@voltx/agents";
78
78
 
79
79
  export const ${toCamelCase(name)} = createAgent({
80
80
  name: "${name}",
81
- model: "cerebras:llama-4-scout-17b-16e",
81
+ model: "cerebras:llama3.1-8b",
82
82
  instructions: "You are a helpful AI assistant named ${name}.",
83
83
  tools: [
84
84
  // Add tools here:
package/dist/generate.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runGenerate
3
- } from "./chunk-AONHLE42.mjs";
3
+ } from "./chunk-HAFJKS2O.mjs";
4
4
  import "./chunk-Y6FXYEAI.mjs";
5
5
  export {
6
6
  runGenerate
package/dist/index.d.mts CHANGED
@@ -4,6 +4,6 @@ export { BuildOptions, runBuild } from './build.mjs';
4
4
  export { StartOptions, runStart } from './start.mjs';
5
5
  export { GenerateOptions, GeneratorType, runGenerate } from './generate.mjs';
6
6
 
7
- declare const CLI_VERSION = "0.3.2";
7
+ declare const CLI_VERSION = "0.3.5";
8
8
 
9
9
  export { CLI_VERSION };
package/dist/index.d.ts CHANGED
@@ -4,6 +4,6 @@ export { BuildOptions, runBuild } from './build.js';
4
4
  export { StartOptions, runStart } from './start.js';
5
5
  export { GenerateOptions, GeneratorType, runGenerate } from './generate.js';
6
6
 
7
- declare const CLI_VERSION = "0.3.2";
7
+ declare const CLI_VERSION = "0.3.5";
8
8
 
9
9
  export { CLI_VERSION };