@yesvara/svara 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/chunk-CIESM3BP.mjs +33 -0
  4. package/dist/chunk-FEA5KIJN.mjs +418 -0
  5. package/dist/cli/index.d.mts +1 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/index.js +328 -0
  8. package/dist/cli/index.mjs +39 -0
  9. package/dist/dev-OYGXXK2B.mjs +69 -0
  10. package/dist/index.d.mts +967 -0
  11. package/dist/index.d.ts +967 -0
  12. package/dist/index.js +1976 -0
  13. package/dist/index.mjs +1502 -0
  14. package/dist/new-7K4NIDZO.mjs +177 -0
  15. package/dist/retriever-4QY667XF.mjs +7 -0
  16. package/examples/01-basic/index.ts +26 -0
  17. package/examples/02-with-tools/index.ts +73 -0
  18. package/examples/03-rag-knowledge/index.ts +41 -0
  19. package/examples/04-multi-channel/index.ts +91 -0
  20. package/package.json +74 -0
  21. package/src/app/index.ts +176 -0
  22. package/src/channels/telegram.ts +122 -0
  23. package/src/channels/web.ts +118 -0
  24. package/src/channels/whatsapp.ts +161 -0
  25. package/src/cli/commands/dev.ts +87 -0
  26. package/src/cli/commands/new.ts +213 -0
  27. package/src/cli/index.ts +78 -0
  28. package/src/core/agent.ts +607 -0
  29. package/src/core/llm.ts +406 -0
  30. package/src/core/types.ts +183 -0
  31. package/src/database/schema.ts +79 -0
  32. package/src/database/sqlite.ts +239 -0
  33. package/src/index.ts +94 -0
  34. package/src/memory/context.ts +49 -0
  35. package/src/memory/conversation.ts +51 -0
  36. package/src/rag/chunker.ts +165 -0
  37. package/src/rag/loader.ts +216 -0
  38. package/src/rag/retriever.ts +248 -0
  39. package/src/tools/executor.ts +54 -0
  40. package/src/tools/index.ts +89 -0
  41. package/src/tools/registry.ts +44 -0
  42. package/src/types.ts +131 -0
  43. package/tsconfig.json +26 -0
@@ -0,0 +1,177 @@
1
+ import "./chunk-CIESM3BP.mjs";
2
+
3
+ // src/cli/commands/new.ts
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+ import { execSync } from "child_process";
7
+ async function newProject(options) {
8
+ const { name, provider = "openai", channels = ["web"] } = options;
9
+ const targetDir = path.resolve(process.cwd(), name);
10
+ console.log(`
11
+ \u2728 Creating SvaraJS project: ${name}
12
+ `);
13
+ try {
14
+ await fs.access(targetDir);
15
+ console.error(`\u274C Directory "${name}" already exists.`);
16
+ process.exit(1);
17
+ } catch {
18
+ }
19
+ await fs.mkdir(targetDir, { recursive: true });
20
+ await fs.mkdir(path.join(targetDir, "src"), { recursive: true });
21
+ await fs.mkdir(path.join(targetDir, "docs"), { recursive: true });
22
+ await fs.mkdir(path.join(targetDir, "data"), { recursive: true });
23
+ const files = {
24
+ "package.json": generatePackageJson(name),
25
+ "tsconfig.json": generateTsConfig(),
26
+ ".env.example": generateEnvExample(provider, channels),
27
+ ".gitignore": generateGitignore(),
28
+ "src/index.ts": generateIndexFile(name, provider, channels),
29
+ "docs/README.md": `# ${name} Knowledge Base
30
+
31
+ Add your documents here for RAG.
32
+ `
33
+ };
34
+ for (const [filePath, content] of Object.entries(files)) {
35
+ const fullPath = path.join(targetDir, filePath);
36
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
37
+ await fs.writeFile(fullPath, content, "utf-8");
38
+ console.log(` \u2713 ${filePath}`);
39
+ }
40
+ if (options.installDeps !== false) {
41
+ console.log("\n\u{1F4E6} Installing dependencies...\n");
42
+ try {
43
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
44
+ } catch {
45
+ console.warn('\n\u26A0\uFE0F Dependency install failed. Run "npm install" manually.\n');
46
+ }
47
+ }
48
+ console.log(`
49
+ \u2705 Project ready!
50
+
51
+ cd ${name}
52
+ cp .env.example .env # Add your API keys
53
+ npm run dev # Start the agent
54
+
55
+ \u{1F4DA} Docs: https://svarajs.dev
56
+ `);
57
+ }
58
+ function generatePackageJson(name) {
59
+ return JSON.stringify({
60
+ name,
61
+ version: "0.1.0",
62
+ private: true,
63
+ scripts: {
64
+ dev: "tsx watch src/index.ts",
65
+ build: "tsc",
66
+ start: "node dist/index.js"
67
+ },
68
+ dependencies: {
69
+ svarajs: "latest",
70
+ dotenv: "^16.4.5"
71
+ },
72
+ devDependencies: {
73
+ "@types/node": "^20.14.2",
74
+ tsx: "^4.15.7",
75
+ typescript: "^5.4.5"
76
+ }
77
+ }, null, 2);
78
+ }
79
+ function generateTsConfig() {
80
+ return JSON.stringify({
81
+ compilerOptions: {
82
+ target: "ES2022",
83
+ module: "CommonJS",
84
+ moduleResolution: "bundler",
85
+ outDir: "dist",
86
+ rootDir: "src",
87
+ strict: true,
88
+ esModuleInterop: true,
89
+ skipLibCheck: true
90
+ },
91
+ include: ["src/**/*"],
92
+ exclude: ["node_modules", "dist"]
93
+ }, null, 2);
94
+ }
95
+ function generateEnvExample(provider, channels) {
96
+ const lines = ["# SvaraJS Environment Variables", ""];
97
+ if (provider === "openai") {
98
+ lines.push("# OpenAI", "OPENAI_API_KEY=sk-...", "");
99
+ } else if (provider === "anthropic") {
100
+ lines.push("# Anthropic", "ANTHROPIC_API_KEY=sk-ant-...", "");
101
+ }
102
+ if (channels.includes("telegram")) {
103
+ lines.push("# Telegram", "TELEGRAM_BOT_TOKEN=...", "");
104
+ }
105
+ if (channels.includes("whatsapp")) {
106
+ lines.push("# WhatsApp", "WA_ACCESS_TOKEN=...", "WA_PHONE_ID=...", "WA_VERIFY_TOKEN=...", "");
107
+ }
108
+ return lines.join("\n");
109
+ }
110
+ function generateIndexFile(name, provider, channels) {
111
+ const modelMap = {
112
+ openai: "gpt-4o",
113
+ anthropic: "claude-opus-4-6",
114
+ ollama: "llama3"
115
+ };
116
+ const channelSetup = channels.map((ch) => {
117
+ if (ch === "web") return `agent.use('web', { port: 3000, cors: true });`;
118
+ if (ch === "telegram") return `agent.use('telegram', { token: process.env.TELEGRAM_BOT_TOKEN! });`;
119
+ if (ch === "whatsapp") return [
120
+ `agent.use('whatsapp', {`,
121
+ ` token: process.env.WA_ACCESS_TOKEN!,`,
122
+ ` phoneId: process.env.WA_PHONE_ID!,`,
123
+ ` verifyToken: process.env.WA_VERIFY_TOKEN!,`,
124
+ `});`
125
+ ].join("\n");
126
+ return "";
127
+ }).filter(Boolean).join("\n");
128
+ return `import 'dotenv/config';
129
+ import { svara } from 'svarajs';
130
+
131
+ /**
132
+ * ${name} \u2014 powered by SvaraJS
133
+ */
134
+ const agent = svara({
135
+ llm: {
136
+ provider: '${provider}',
137
+ model: '${modelMap[provider] ?? "gpt-4o"}',
138
+ },
139
+ systemPrompt: \`You are a helpful AI assistant called ${name}.
140
+ Be concise, friendly, and always try your best to help.\`,
141
+ memory: {
142
+ type: 'conversation',
143
+ maxMessages: 20,
144
+ },
145
+ verbose: true,
146
+ });
147
+
148
+ // \u2500\u2500 Tools \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\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
149
+ agent.tool('get_time', {
150
+ description: 'Get the current date and time',
151
+ parameters: {},
152
+ execute: async () => ({
153
+ datetime: new Date().toISOString(),
154
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
155
+ }),
156
+ });
157
+
158
+ // \u2500\u2500 Channels \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
159
+ ${channelSetup}
160
+
161
+ // \u2500\u2500 Start \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\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
162
+ agent.start().then(() => {
163
+ console.log('\u{1F680} ${name} is running!');
164
+ });
165
+ `;
166
+ }
167
+ function generateGitignore() {
168
+ return `node_modules/
169
+ dist/
170
+ .env
171
+ data/*.db
172
+ *.log
173
+ `;
174
+ }
175
+ export {
176
+ newProject
177
+ };
@@ -0,0 +1,7 @@
1
+ import {
2
+ VectorRetriever
3
+ } from "./chunk-FEA5KIJN.mjs";
4
+ import "./chunk-CIESM3BP.mjs";
5
+ export {
6
+ VectorRetriever
7
+ };
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @example Basic Agent
3
+ *
4
+ * The simplest possible SvaraJS agent.
5
+ * 10 lines. Works out of the box.
6
+ *
7
+ * Run: npx tsx index.ts
8
+ *
9
+ * curl -X POST http://localhost:3000/chat \
10
+ * -H "Content-Type: application/json" \
11
+ * -d '{ "message": "Hello! What can you do?" }'
12
+ */
13
+
14
+ import 'dotenv/config';
15
+ import { SvaraApp, SvaraAgent } from '@yesvara/svara';
16
+
17
+ const app = new SvaraApp({ cors: true });
18
+
19
+ const agent = new SvaraAgent({
20
+ name: 'Aria',
21
+ model: 'gpt-4o-mini',
22
+ systemPrompt: 'You are Aria, a friendly and helpful AI assistant. Keep responses concise.',
23
+ });
24
+
25
+ app.route('/chat', agent.handler());
26
+ app.listen(3000);
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @example Agent with Tools
3
+ *
4
+ * An agent that can call custom functions (tools).
5
+ * The LLM decides when to call each tool based on the conversation.
6
+ *
7
+ * Run: npx tsx index.ts
8
+ *
9
+ * curl -X POST http://localhost:3000/chat \
10
+ * -H "Content-Type: application/json" \
11
+ * -d '{ "message": "What time is it? Also, what is 42 * 17?", "sessionId": "user-1" }'
12
+ */
13
+
14
+ import 'dotenv/config';
15
+ import { SvaraApp, SvaraAgent, createTool } from '@yesvara/svara';
16
+
17
+ // ── Define Tools ──────────────────────────────────────────────────────────────
18
+
19
+ const getCurrentTime = createTool({
20
+ name: 'get_current_time',
21
+ description: 'Get the current date and time in a specific timezone',
22
+ parameters: {
23
+ timezone: {
24
+ type: 'string',
25
+ description: 'IANA timezone name, e.g. "Asia/Jakarta", "America/New_York"',
26
+ },
27
+ },
28
+ async run({ timezone = 'UTC' }) {
29
+ return {
30
+ datetime: new Date().toLocaleString('en-US', { timeZone: timezone as string }),
31
+ timezone,
32
+ };
33
+ },
34
+ });
35
+
36
+ const calculate = createTool({
37
+ name: 'calculate',
38
+ description: 'Evaluate a safe mathematical expression',
39
+ parameters: {
40
+ expression: {
41
+ type: 'string',
42
+ description: 'Math expression to evaluate, e.g. "42 * 17", "(100 + 50) / 3"',
43
+ required: true,
44
+ },
45
+ },
46
+ async run({ expression }) {
47
+ // Very simple safe eval — in production use a proper math library
48
+ const result = Function(`"use strict"; return (${expression as string})`)() as number;
49
+ return { expression, result };
50
+ },
51
+ timeout: 5_000,
52
+ });
53
+
54
+ // ── Create Agent ──────────────────────────────────────────────────────────────
55
+
56
+ const agent = new SvaraAgent({
57
+ name: 'Calculator Bot',
58
+ model: 'gpt-4o-mini',
59
+ systemPrompt: 'You are a helpful assistant with access to real-time data and calculation tools.',
60
+ memory: { window: 10 },
61
+ tools: [getCurrentTime, calculate],
62
+ });
63
+
64
+ // ── Start ─────────────────────────────────────────────────────────────────────
65
+
66
+ const app = new SvaraApp({ cors: true });
67
+ app.route('/chat', agent.handler());
68
+ app.listen(3000);
69
+
70
+ // Observe tool usage
71
+ agent.on('tool:call', ({ tools }: { tools: string[] }) => {
72
+ console.log(`[Tools] Calling: ${tools.join(', ')}`);
73
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @example RAG Knowledge Base Agent
3
+ *
4
+ * An agent that answers questions from your documents.
5
+ * Drop files in ./docs and the agent knows everything in them.
6
+ *
7
+ * Supported: PDF, Markdown, TXT, DOCX, HTML, JSON
8
+ *
9
+ * Run: npx tsx index.ts
10
+ *
11
+ * curl -X POST http://localhost:3000/chat \
12
+ * -H "Content-Type: application/json" \
13
+ * -d '{ "message": "What is our refund policy?", "sessionId": "customer-42" }'
14
+ */
15
+
16
+ import 'dotenv/config';
17
+ import { SvaraApp, SvaraAgent } from '@yesvara/svara';
18
+
19
+ const agent = new SvaraAgent({
20
+ name: 'Knowledge Base Bot',
21
+ model: 'gpt-4o-mini',
22
+
23
+ systemPrompt: `You are a helpful customer support agent.
24
+ Answer questions using the provided documentation.
25
+ If you don't know the answer, say so honestly — don't make things up.
26
+ Always be friendly and professional.`,
27
+
28
+ // Point to your docs folder — any file type, glob patterns work
29
+ knowledge: './docs/**/*',
30
+
31
+ memory: { window: 20 },
32
+ });
33
+
34
+ // You can also add knowledge dynamically (hot reload, no restart needed)
35
+ // await agent.addKnowledge('./new-policy-2024.pdf');
36
+
37
+ const app = new SvaraApp({ cors: true });
38
+ app.route('/chat', agent.handler());
39
+
40
+ await agent.start(); // indexes documents
41
+ app.listen(3000);
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @example Multi-Channel Agent
3
+ *
4
+ * One agent, three channels. Same conversations, everywhere.
5
+ * Customers can reach you on WhatsApp, Telegram, or the web —
6
+ * the agent handles all of them seamlessly.
7
+ *
8
+ * Setup:
9
+ * 1. Copy .env.example → .env and fill in your keys
10
+ * 2. Run: npx tsx index.ts
11
+ * 3. Expose with: npx localtunnel --port 3000
12
+ * 4. Register the tunnel URL as your Telegram/WhatsApp webhook
13
+ */
14
+
15
+ import 'dotenv/config';
16
+ import { SvaraAgent, createTool } from '@yesvara/svara';
17
+
18
+ // ── Tools ─────────────────────────────────────────────────────────────────────
19
+
20
+ const orderStatus = createTool({
21
+ name: 'get_order_status',
22
+ description: 'Look up the status of a customer order by order ID',
23
+ parameters: {
24
+ orderId: { type: 'string', description: 'The order ID', required: true },
25
+ },
26
+ async run({ orderId }) {
27
+ // Replace with your real database query
28
+ const mockOrders: Record<string, unknown> = {
29
+ 'ORD-001': { status: 'shipped', eta: '2024-12-25', carrier: 'FedEx' },
30
+ 'ORD-002': { status: 'processing', eta: '2024-12-27', carrier: null },
31
+ };
32
+ return mockOrders[orderId as string] ?? { error: 'Order not found' };
33
+ },
34
+ });
35
+
36
+ // ── Agent ─────────────────────────────────────────────────────────────────────
37
+
38
+ const agent = new SvaraAgent({
39
+ name: 'Aria Support',
40
+ model: 'gpt-4o-mini',
41
+ knowledge: './policies', // auto-indexed on start()
42
+
43
+ systemPrompt: `You are Aria, the customer support assistant for Acme Store.
44
+ You are helpful, empathetic, and solution-focused.
45
+ You can check order status and answer questions from our knowledge base.
46
+ Always greet customers by name if they provide it.`,
47
+
48
+ memory: { window: 30 },
49
+ tools: [orderStatus],
50
+ });
51
+
52
+ // ── Channels ──────────────────────────────────────────────────────────────────
53
+
54
+ agent
55
+ // Web API — for website chat widget
56
+ .connectChannel('web', {
57
+ port: 3000,
58
+ cors: true,
59
+ })
60
+
61
+ // Telegram Bot
62
+ .connectChannel('telegram', {
63
+ token: process.env.TELEGRAM_BOT_TOKEN!,
64
+ mode: 'polling', // use 'webhook' in production
65
+ })
66
+
67
+ // WhatsApp Business
68
+ .connectChannel('whatsapp', {
69
+ token: process.env.WA_ACCESS_TOKEN!,
70
+ phoneId: process.env.WA_PHONE_ID!,
71
+ verifyToken: process.env.WA_VERIFY_TOKEN!,
72
+ });
73
+
74
+ // ── Events ────────────────────────────────────────────────────────────────────
75
+
76
+ agent.on('message:received', ({ message, sessionId }: { message: string; sessionId: string }) => {
77
+ console.log(`[${sessionId.slice(0, 8)}] User: ${message.slice(0, 50)}...`);
78
+ });
79
+
80
+ agent.on('tool:call', ({ tools }: { tools: string[] }) => {
81
+ console.log(`[Tools] → ${tools.join(', ')}`);
82
+ });
83
+
84
+ agent.on('channel:ready', ({ channel }: { channel: string }) => {
85
+ console.log(`[Channel] ${channel} is ready`);
86
+ });
87
+
88
+ // ── Start ─────────────────────────────────────────────────────────────────────
89
+
90
+ await agent.start();
91
+ console.log('\n🚀 Aria Support is live across all channels!\n');
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@yesvara/svara",
3
+ "version": "0.1.0",
4
+ "description": "Build AI agents in 15 lines of code. Multi-channel, RAG-ready, production-grade.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "svara": "./dist/cli/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsup src/index.ts src/cli/index.ts --format cjs,esm --dts --clean",
13
+ "dev": "tsup src/index.ts --watch --format cjs",
14
+ "test": "vitest",
15
+ "typecheck": "tsc --noEmit",
16
+ "prepublishOnly": "npm run build && npm run typecheck"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.mjs",
21
+ "require": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ }
24
+ },
25
+ "keywords": [
26
+ "ai", "agent", "llm", "openai", "anthropic", "rag",
27
+ "chatbot", "whatsapp", "telegram", "framework", "agentic"
28
+ ],
29
+ "author": "Yesvara Contributors",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/yogiswara92/svara"
34
+ },
35
+ "homepage": "https://svarajs.yesvara.com",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "express": "^4.18.2",
41
+ "better-sqlite3": "^9.4.3",
42
+ "openai": "^4.47.1",
43
+ "@anthropic-ai/sdk": "^0.24.3",
44
+ "zod": "^3.23.8",
45
+ "chalk": "^5.3.0",
46
+ "commander": "^12.1.0",
47
+ "dotenv": "^16.4.5",
48
+ "glob": "^10.4.1",
49
+ "pdf-parse": "^1.1.1",
50
+ "mammoth": "^1.7.2",
51
+ "ora": "^8.0.1",
52
+ "inquirer": "^9.3.2"
53
+ },
54
+ "devDependencies": {
55
+ "@types/express": "^4.17.21",
56
+ "@types/better-sqlite3": "^7.6.10",
57
+ "@types/node": "^20.14.2",
58
+ "@types/pdf-parse": "^1.1.4",
59
+ "tsup": "^8.1.0",
60
+ "typescript": "^5.4.5",
61
+ "vitest": "^1.6.0"
62
+ },
63
+ "peerDependencies": {
64
+ "openai": ">=4.0.0",
65
+ "@anthropic-ai/sdk": ">=0.20.0"
66
+ },
67
+ "peerDependenciesMeta": {
68
+ "openai": { "optional": true },
69
+ "@anthropic-ai/sdk": { "optional": true }
70
+ },
71
+ "engines": {
72
+ "node": ">=18.0.0"
73
+ }
74
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @module SvaraApp
3
+ *
4
+ * The framework entry point. Wraps Express to give you a clean,
5
+ * AI-first HTTP server that works with zero configuration.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { SvaraApp, SvaraAgent } from '@yesvara/svara';
10
+ *
11
+ * const app = new SvaraApp();
12
+ *
13
+ * const agent = new SvaraAgent({
14
+ * name: 'Support Bot',
15
+ * model: 'gpt-4o-mini',
16
+ * knowledge: './docs',
17
+ * });
18
+ *
19
+ * app.route('/chat', agent.handler());
20
+ * app.listen(3000);
21
+ * // → Server running at http://localhost:3000
22
+ * // → POST /chat accepts { message, sessionId }
23
+ * // → GET /health returns { status: 'ok' }
24
+ * ```
25
+ *
26
+ * @example With your own Express app
27
+ * ```ts
28
+ * import express from 'express';
29
+ * const expressApp = express();
30
+ *
31
+ * // Mount as middleware on any path
32
+ * expressApp.post('/api/chat', agent.handler());
33
+ * ```
34
+ */
35
+
36
+ import express, { type Express, type RequestHandler, type Request, type Response } from 'express';
37
+ import { createServer, type Server } from 'http';
38
+
39
+ export interface AppOptions {
40
+ /**
41
+ * Enable CORS. Pass `true` for wildcard (*), or a specific origin string.
42
+ * @default false
43
+ */
44
+ cors?: boolean | string;
45
+
46
+ /**
47
+ * Require an API key in the `Authorization: Bearer <key>` header.
48
+ * Useful for protecting your agent endpoint.
49
+ */
50
+ apiKey?: string;
51
+
52
+ /**
53
+ * Request body size limit. @default '10mb'
54
+ */
55
+ bodyLimit?: string;
56
+ }
57
+
58
+ export class SvaraApp {
59
+ private readonly express: Express;
60
+ private server: Server | null = null;
61
+
62
+ constructor(options: AppOptions = {}) {
63
+ this.express = express();
64
+ this.setup(options);
65
+ }
66
+
67
+ // ─── Public API ────────────────────────────────────────────────────────────
68
+
69
+ /**
70
+ * Mount an agent (or any Express handler) on a route.
71
+ * Returns `this` for chaining.
72
+ *
73
+ * @example
74
+ * app
75
+ * .route('/chat', supportAgent.handler())
76
+ * .route('/sales', salesAgent.handler());
77
+ */
78
+ route(path: string, handler: RequestHandler): this {
79
+ this.express.post(path, handler);
80
+ return this;
81
+ }
82
+
83
+ /**
84
+ * Add Express middleware (logging, auth, rate limiting, etc.)
85
+ *
86
+ * @example
87
+ * import rateLimit from 'express-rate-limit';
88
+ * app.use(rateLimit({ windowMs: 60_000, max: 100 }));
89
+ */
90
+ use(middleware: RequestHandler): this {
91
+ this.express.use(middleware);
92
+ return this;
93
+ }
94
+
95
+ /**
96
+ * Start listening on the given port.
97
+ *
98
+ * @example
99
+ * await app.listen(3000);
100
+ * // → [@yesvara/svara] Listening at http://localhost:3000
101
+ */
102
+ listen(port = 3000): Promise<void> {
103
+ return new Promise((resolve, reject) => {
104
+ this.server = createServer(this.express);
105
+ this.server.listen(port, () => {
106
+ console.log(`[@yesvara/svara] Server running at http://localhost:${port}`);
107
+ resolve();
108
+ });
109
+ this.server.on('error', reject);
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Stop the server gracefully.
115
+ */
116
+ stop(): Promise<void> {
117
+ return new Promise((resolve) => {
118
+ if (this.server) {
119
+ this.server.close(() => resolve());
120
+ } else {
121
+ resolve();
122
+ }
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Access the underlying Express app for advanced configuration.
128
+ *
129
+ * @example
130
+ * const expressApp = app.express();
131
+ * expressApp.set('trust proxy', 1);
132
+ */
133
+ getExpressApp(): Express {
134
+ return this.express;
135
+ }
136
+
137
+ // ─── Private Setup ────────────────────────────────────────────────────────
138
+
139
+ private setup(options: AppOptions): void {
140
+ // Parse JSON bodies
141
+ this.express.use(express.json({ limit: options.bodyLimit ?? '10mb' }));
142
+
143
+ // CORS
144
+ if (options.cors) {
145
+ this.express.use((_req, res, next) => {
146
+ const origin = options.cors === true ? '*' : options.cors as string;
147
+ res.setHeader('Access-Control-Allow-Origin', origin);
148
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
149
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
150
+ next();
151
+ });
152
+ this.express.options('*', (_req, res) => res.sendStatus(204));
153
+ }
154
+
155
+ // API key auth
156
+ if (options.apiKey) {
157
+ this.express.use((req, res, next) => {
158
+ const token = req.headers.authorization?.replace('Bearer ', '');
159
+ if (token !== options.apiKey) {
160
+ res.status(401).json({ error: 'Unauthorized', message: 'Invalid API key.' });
161
+ return;
162
+ }
163
+ next();
164
+ });
165
+ }
166
+
167
+ // Health check — always available, no auth
168
+ this.express.get('/health', (_req: Request, res: Response) => {
169
+ res.json({
170
+ status: 'ok',
171
+ framework: '@yesvara/svara',
172
+ timestamp: new Date().toISOString(),
173
+ });
174
+ });
175
+ }
176
+ }