voyageai-cli 1.23.1 → 1.26.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 (46) hide show
  1. package/README.md +64 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +2 -0
  4. package/src/commands/about.js +1 -1
  5. package/src/commands/bug.js +1 -1
  6. package/src/commands/mcp-server.js +74 -0
  7. package/src/commands/playground.js +31 -0
  8. package/src/commands/scaffold.js +23 -1
  9. package/src/commands/workflow.js +336 -0
  10. package/src/lib/explanations.js +53 -0
  11. package/src/lib/scaffold-structure.js +8 -9
  12. package/src/lib/telemetry.js +1 -1
  13. package/src/lib/template-engine.js +240 -0
  14. package/src/lib/templates/nextjs/README.md.tpl +78 -55
  15. package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
  16. package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
  17. package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
  18. package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
  19. package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
  20. package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
  21. package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
  22. package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
  23. package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
  24. package/src/lib/templates/nextjs/theme.js.tpl +138 -65
  25. package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
  26. package/src/lib/workflow-utils.js +65 -0
  27. package/src/lib/workflow.js +1259 -0
  28. package/src/mcp/install.js +201 -0
  29. package/src/mcp/tools/management.js +1 -60
  30. package/src/playground/icons/dark/128.png +0 -0
  31. package/src/playground/icons/dark/16.png +0 -0
  32. package/src/playground/icons/dark/256.png +0 -0
  33. package/src/playground/icons/dark/32.png +0 -0
  34. package/src/playground/icons/dark/64.png +0 -0
  35. package/src/playground/icons/light/128.png +0 -0
  36. package/src/playground/icons/light/16.png +0 -0
  37. package/src/playground/icons/light/256.png +0 -0
  38. package/src/playground/icons/light/32.png +0 -0
  39. package/src/playground/icons/light/64.png +0 -0
  40. package/src/playground/icons/watermark.png +0 -0
  41. package/src/playground/index.html +125 -73
  42. package/src/workflows/consistency-check.json +64 -0
  43. package/src/workflows/cost-analysis.json +69 -0
  44. package/src/workflows/multi-collection-search.json +80 -0
  45. package/src/workflows/research-and-summarize.json +46 -0
  46. package/src/workflows/smart-ingest.json +63 -0
@@ -1434,6 +1434,50 @@ const concepts = {
1434
1434
  'vai chat --db myapp --collection knowledge',
1435
1435
  ],
1436
1436
  },
1437
+
1438
+ workflows: {
1439
+ title: 'Agentic Workflows',
1440
+ summary: 'Composable, multi-step RAG pipelines as JSON files',
1441
+ content: [
1442
+ `${pc.cyan('Workflows')} are composable, multi-step RAG pipelines defined as portable`,
1443
+ `JSON files. Think Docker Compose or GitHub Actions, but for search and retrieval`,
1444
+ `pipelines.`,
1445
+ ``,
1446
+ `${pc.bold('Why workflows?')} Instead of writing bash scripts to chain vai commands,`,
1447
+ `a workflow file captures the intent declaratively. Workflows are reproducible,`,
1448
+ `shareable (commit them to git), and inspectable.`,
1449
+ ``,
1450
+ `${pc.bold('How it works:')}`,
1451
+ `Each workflow defines a DAG (directed acyclic graph) of steps. Each step maps`,
1452
+ `to a vai operation (query, search, rerank, embed, etc.) or a control flow`,
1453
+ `operation (merge, filter, transform, generate). Steps reference outputs from`,
1454
+ `previous steps using ${pc.cyan('{{ template expressions }}')} for data flow.`,
1455
+ ``,
1456
+ `${pc.bold('Template expressions:')}`,
1457
+ ` ${pc.cyan('{{ inputs.query }}')} workflow input parameter`,
1458
+ ` ${pc.cyan('{{ search.output.results }}')} results from a previous step`,
1459
+ ` ${pc.cyan('{{ merge.output.results[0] }}')} array indexing`,
1460
+ ` ${pc.cyan('{{ defaults.db }}')} workflow default value`,
1461
+ ``,
1462
+ `${pc.bold('Parallel execution:')} The engine automatically detects independent steps`,
1463
+ `and runs them in parallel. No configuration needed.`,
1464
+ ``,
1465
+ `${pc.bold('Step types:')}`,
1466
+ ` ${pc.dim('VAI tools:')} query, search, rerank, embed, similarity, ingest,`,
1467
+ ` collections, models, explain, estimate`,
1468
+ ` ${pc.dim('Control flow:')} merge, filter, transform, generate`,
1469
+ ``,
1470
+ `${pc.bold('Built-in templates:')} Run ${pc.cyan('vai workflow list')} to see available`,
1471
+ `templates like multi-collection-search, smart-ingest, research-and-summarize.`,
1472
+ ].join('\n'),
1473
+ links: ['https://github.com/mrlynn/voyageai-cli#workflows'],
1474
+ tryIt: [
1475
+ 'vai workflow list',
1476
+ 'vai workflow init --name my-pipeline',
1477
+ 'vai workflow validate ./my-pipeline.vai-workflow.json',
1478
+ 'vai workflow run multi-collection-search --input query="test" --dry-run',
1479
+ ],
1480
+ },
1437
1481
  };
1438
1482
 
1439
1483
  /**
@@ -1598,6 +1642,15 @@ const aliases = {
1598
1642
  conversational: 'chat',
1599
1643
  'chat-history': 'chat',
1600
1644
  llm: 'chat',
1645
+ // Workflow aliases
1646
+ workflow: 'workflows',
1647
+ workflows: 'workflows',
1648
+ 'rag-pipeline': 'workflows',
1649
+ composable: 'workflows',
1650
+ 'workflow-engine': 'workflows',
1651
+ 'vai-workflow': 'workflows',
1652
+ pipeline: 'workflows',
1653
+ dag: 'workflows',
1601
1654
  };
1602
1655
 
1603
1656
  /**
@@ -31,23 +31,22 @@ const PROJECT_STRUCTURE = {
31
31
  { template: 'env.example', output: '.env.example' },
32
32
  { template: 'README.md', output: 'README.md' },
33
33
  { template: 'layout.jsx', output: 'app/layout.jsx' },
34
+ { template: 'page-home.jsx', output: 'app/page.jsx' },
34
35
  { template: 'page-search.jsx', output: 'app/search/page.jsx' },
35
36
  { template: 'route-search.js', output: 'app/api/search/route.js' },
36
37
  { template: 'route-ingest.js', output: 'app/api/ingest/route.js' },
37
38
  { template: 'lib-voyage.js', output: 'lib/voyage.js' },
38
39
  { template: 'lib-mongo.js', output: 'lib/mongodb.js' },
39
40
  { template: 'theme.js', output: 'lib/theme.js' },
41
+ { template: 'theme-registry.jsx', output: 'components/ThemeRegistry.jsx' },
42
+ { template: 'navbar.jsx', output: 'components/Navbar.jsx' },
43
+ { template: 'footer.jsx', output: 'components/Footer.jsx' },
44
+ { template: 'favicon.svg', output: 'public/favicon.svg' },
45
+ ],
46
+ binaryFiles: [
47
+ { source: 'vai-logo-256.png', output: 'public/vai-logo.png' },
40
48
  ],
41
49
  extraFiles: [
42
- {
43
- output: 'app/page.jsx',
44
- content: `'use client';
45
- import { redirect } from 'next/navigation';
46
- export default function Home() {
47
- redirect('/search');
48
- }
49
- `,
50
- },
51
50
  {
52
51
  output: 'next.config.js',
53
52
  content: `/** @type {import('next').NextConfig} */
@@ -9,7 +9,7 @@
9
9
  * - command name, version, platform, locale
10
10
  */
11
11
 
12
- const TELEMETRY_URL = 'https://vai.mlynn.org/api/telemetry';
12
+ const TELEMETRY_URL = 'https://vaicli.com/api/telemetry';
13
13
  const TIMEOUT_MS = 3000;
14
14
 
15
15
  /**
@@ -0,0 +1,240 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Template expression engine for vai workflows.
5
+ *
6
+ * Resolves {{ expression }} strings against a context object.
7
+ * Supports dot-path access and array indexing only.
8
+ * No eval(), no function calls, no arithmetic.
9
+ *
10
+ * Grammar:
11
+ * expression = segment ("." segment)*
12
+ * segment = identifier ("[" index "]")?
13
+ * identifier = [a-zA-Z_][a-zA-Z0-9_]*
14
+ * index = [0-9]+
15
+ */
16
+
17
+ // Matches {{ path.to.value }} including {{ path[0].field }}
18
+ const TEMPLATE_RE = /\{\{\s*(.+?)\s*\}\}/g;
19
+
20
+ // A string that is exactly one template expression with no surrounding text
21
+ // Use [^}] to prevent matching across multiple {{ }} pairs
22
+ const SOLE_TEMPLATE_RE = /^\{\{\s*([^}]+?)\s*\}\}$/;
23
+
24
+ /**
25
+ * Check if a string contains template expressions.
26
+ * @param {string} str
27
+ * @returns {boolean}
28
+ */
29
+ function isTemplateString(str) {
30
+ if (typeof str !== 'string') return false;
31
+ // Use a fresh regex to avoid global lastIndex state issues
32
+ return /\{\{\s*(.+?)\s*\}\}/.test(str);
33
+ }
34
+
35
+ /**
36
+ * Parse a dot-path expression into segments.
37
+ *
38
+ * @param {string} expr - e.g. "search_api.output.results[0].content"
39
+ * @returns {Array<{key: string, index?: number}>}
40
+ * @throws {Error} on invalid syntax
41
+ */
42
+ function parseExpression(expr) {
43
+ const trimmed = expr.trim();
44
+ if (!trimmed) throw new Error('Empty expression');
45
+
46
+ const segments = [];
47
+ const parts = trimmed.split('.');
48
+
49
+ for (const part of parts) {
50
+ // Check for array indexing: identifier[index]
51
+ const bracketMatch = part.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\[(\d+)\]$/);
52
+ if (bracketMatch) {
53
+ segments.push({ key: bracketMatch[1], index: parseInt(bracketMatch[2], 10) });
54
+ continue;
55
+ }
56
+
57
+ // Plain identifier
58
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(part)) {
59
+ segments.push({ key: part });
60
+ continue;
61
+ }
62
+
63
+ throw new Error(`Invalid expression segment: "${part}" in "${expr}"`);
64
+ }
65
+
66
+ return segments;
67
+ }
68
+
69
+ /**
70
+ * Resolve a parsed path against a context object.
71
+ * Returns undefined (no throw) if any segment is missing.
72
+ *
73
+ * @param {Array<{key: string, index?: number}>} segments
74
+ * @param {object} context
75
+ * @returns {*}
76
+ */
77
+ function resolvePath(segments, context) {
78
+ let current = context;
79
+
80
+ for (const seg of segments) {
81
+ if (current == null || typeof current !== 'object') return undefined;
82
+
83
+ current = current[seg.key];
84
+ if (current == null) return current; // null or undefined
85
+
86
+ if (seg.index !== undefined) {
87
+ if (!Array.isArray(current)) return undefined;
88
+ current = current[seg.index];
89
+ }
90
+ }
91
+
92
+ return current;
93
+ }
94
+
95
+ /**
96
+ * Resolve template expressions within a single string.
97
+ *
98
+ * If the entire string is a single template expression, return the resolved
99
+ * value directly (preserving type: array, number, object, etc.).
100
+ * If the string contains templates mixed with text, return a string with
101
+ * substitutions.
102
+ *
103
+ * @param {string} str
104
+ * @param {object} context
105
+ * @returns {*}
106
+ */
107
+ function resolveString(str, context) {
108
+ // Fast path: no templates
109
+ if (!str.includes('{{')) return str;
110
+
111
+ // Check if the entire string is a single template expression
112
+ const soleMatch = str.match(SOLE_TEMPLATE_RE);
113
+ if (soleMatch) {
114
+ try {
115
+ const segments = parseExpression(soleMatch[1]);
116
+ return resolvePath(segments, context);
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ }
121
+
122
+ // Mixed text + templates: substitute all, coerce to string
123
+ return str.replace(TEMPLATE_RE, (_match, expr) => {
124
+ try {
125
+ const segments = parseExpression(expr);
126
+ const value = resolvePath(segments, context);
127
+ if (value === undefined) return '';
128
+ if (value === null) return 'null';
129
+ if (typeof value === 'object') return JSON.stringify(value);
130
+ return String(value);
131
+ } catch {
132
+ return '';
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Recursively resolve all template expressions in a value.
139
+ * Handles strings, arrays, and plain objects.
140
+ *
141
+ * @param {*} value
142
+ * @param {object} context
143
+ * @returns {*}
144
+ */
145
+ function resolveTemplate(value, context) {
146
+ if (typeof value === 'string') {
147
+ return resolveString(value, context);
148
+ }
149
+
150
+ if (Array.isArray(value)) {
151
+ return value.map(item => resolveTemplate(item, context));
152
+ }
153
+
154
+ if (value !== null && typeof value === 'object') {
155
+ const resolved = {};
156
+ for (const [k, v] of Object.entries(value)) {
157
+ resolved[k] = resolveTemplate(v, context);
158
+ }
159
+ return resolved;
160
+ }
161
+
162
+ // numbers, booleans, null, undefined pass through
163
+ return value;
164
+ }
165
+
166
+ /**
167
+ * Deep-resolve all template expressions in an object tree.
168
+ * Alias for resolveTemplate when called on an object.
169
+ *
170
+ * @param {object} obj
171
+ * @param {object} context
172
+ * @returns {object}
173
+ */
174
+ function resolveAllTemplates(obj, context) {
175
+ return resolveTemplate(obj, context);
176
+ }
177
+
178
+ /**
179
+ * Extract step IDs referenced by template expressions in an object tree.
180
+ * Ignores "inputs" and "defaults" prefixes (those are workflow-level, not step refs).
181
+ *
182
+ * @param {*} obj - Step inputs, condition, forEach value
183
+ * @returns {Set<string>} Set of step IDs referenced
184
+ */
185
+ function extractDependencies(obj) {
186
+ const deps = new Set();
187
+
188
+ function scan(value) {
189
+ if (typeof value === 'string') {
190
+ // Reset regex state
191
+ const re = /\{\{\s*(.+?)\s*\}\}/g;
192
+ let match;
193
+ while ((match = re.exec(value)) !== null) {
194
+ const expr = match[1].trim();
195
+ // Extract the first segment (root identifier)
196
+ const dotIdx = expr.indexOf('.');
197
+ const bracketIdx = expr.indexOf('[');
198
+ let root;
199
+ if (dotIdx === -1 && bracketIdx === -1) {
200
+ root = expr;
201
+ } else if (dotIdx === -1) {
202
+ root = expr.slice(0, bracketIdx);
203
+ } else if (bracketIdx === -1) {
204
+ root = expr.slice(0, dotIdx);
205
+ } else {
206
+ root = expr.slice(0, Math.min(dotIdx, bracketIdx));
207
+ }
208
+
209
+ // Skip workflow-level references
210
+ if (root !== 'inputs' && root !== 'defaults') {
211
+ deps.add(root);
212
+ }
213
+ }
214
+ return;
215
+ }
216
+
217
+ if (Array.isArray(value)) {
218
+ value.forEach(scan);
219
+ return;
220
+ }
221
+
222
+ if (value !== null && typeof value === 'object') {
223
+ Object.values(value).forEach(scan);
224
+ }
225
+ }
226
+
227
+ scan(obj);
228
+ return deps;
229
+ }
230
+
231
+ module.exports = {
232
+ isTemplateString,
233
+ parseExpression,
234
+ resolvePath,
235
+ resolveString,
236
+ resolveTemplate,
237
+ resolveAllTemplates,
238
+ extractDependencies,
239
+ TEMPLATE_RE,
240
+ };
@@ -1,13 +1,39 @@
1
- # {{projectName}}
1
+ # 🔍 {{projectName}}
2
2
 
3
- A semantic search application powered by Voyage AI embeddings, MongoDB Atlas Vector Search, and Next.js with Material UI.
3
+ > Semantic search application powered by **Voyage AI** embeddings, **MongoDB Atlas Vector Search**, and **Next.js** with Material UI.
4
4
 
5
- ## Configuration
5
+ [![Generated by vai](https://img.shields.io/badge/scaffolded%20with-vai-00ED64?style=flat-square)](https://github.com/mrlynn/voyageai-cli)
6
+ [![Next.js](https://img.shields.io/badge/Next.js-14-black?style=flat-square&logo=next.js)](https://nextjs.org)
7
+ [![MUI](https://img.shields.io/badge/MUI-5-007FFF?style=flat-square&logo=mui)](https://mui.com)
8
+
9
+ ---
10
+
11
+ ## ⚡ Quick Start
12
+
13
+ ```bash
14
+ # 1. Install
15
+ npm install
16
+
17
+ # 2. Configure
18
+ cp .env.example .env.local
19
+ # → Edit .env.local with your Voyage AI key + MongoDB URI
20
+
21
+ # 3. Create vector index (see below)
22
+
23
+ # 4. Run
24
+ npm run dev
25
+ ```
26
+
27
+ Open **[http://localhost:3000](http://localhost:3000)** — you'll see a branded landing page with a link to the search UI.
28
+
29
+ ---
30
+
31
+ ## 🔧 Configuration
6
32
 
7
33
  | Setting | Value |
8
34
  |---------|-------|
9
35
  | Embedding Model | `{{model}}` |
10
- | Dimensions | {{dimensions}} |
36
+ | Dimensions | `{{dimensions}}` |
11
37
  | Database | `{{db}}` |
12
38
  | Collection | `{{collection}}` |
13
39
  | Vector Index | `{{index}}` |
@@ -15,77 +41,74 @@ A semantic search application powered by Voyage AI embeddings, MongoDB Atlas Vec
15
41
  | Rerank Model | `{{rerankModel}}` |
16
42
  {{/if}}
17
43
 
18
- ## Setup
19
-
20
- ### 1. Install dependencies
21
-
22
- ```bash
23
- npm install
24
- ```
25
-
26
- ### 2. Configure environment
27
-
28
- Copy `.env.example` to `.env.local` and fill in your credentials:
44
+ ### Environment Variables
29
45
 
30
- ```bash
31
- cp .env.example .env.local
46
+ ```env
47
+ VOYAGE_API_KEY= # From dash.voyageai.com
48
+ MONGODB_URI= # Atlas connection string
32
49
  ```
33
50
 
34
- Required variables:
35
- - `VOYAGE_API_KEY` - Your Voyage AI API key from [dash.voyageai.com](https://dash.voyageai.com)
36
- - `MONGODB_URI` - Your MongoDB Atlas connection string
51
+ ### Vector Search Index
37
52
 
38
- ### 3. Create vector index
39
-
40
- In MongoDB Atlas, create a vector search index on your collection:
53
+ Create this index on your `{{collection}}` collection in Atlas:
41
54
 
42
55
  ```json
43
56
  {
44
- "fields": [
45
- {
46
- "type": "vector",
47
- "path": "{{field}}",
48
- "numDimensions": {{dimensions}},
49
- "similarity": "cosine"
50
- }
51
- ]
57
+ "fields": [{
58
+ "type": "vector",
59
+ "path": "{{field}}",
60
+ "numDimensions": {{dimensions}},
61
+ "similarity": "cosine"
62
+ }]
52
63
  }
53
64
  ```
54
65
 
55
- Name the index `{{index}}`.
56
-
57
- ### 4. Start the development server
58
-
59
- ```bash
60
- npm run dev
61
- ```
66
+ Name it **`{{index}}`**.
62
67
 
63
- Open [http://localhost:3000](http://localhost:3000) to see the search interface.
68
+ ---
64
69
 
65
- ## Project Structure
70
+ ## 📁 Project Structure
66
71
 
67
72
  ```
68
73
  {{projectName}}/
69
74
  ├── app/
70
- │ ├── layout.jsx # Root layout with MUI theme
71
- │ ├── page.jsx # Home page
72
- │ ├── search/
73
- │ │ └── page.jsx # Search page (MUI)
75
+ │ ├── layout.jsx # Root layout (Inter font, theme)
76
+ │ ├── page.jsx # Landing page (hero + features)
77
+ │ ├── search/page.jsx # Search UI (score bars, copy, fade-in)
74
78
  │ └── api/
75
- │ ├── search/route.js # Search endpoint
76
- │ └── ingest/route.js # Ingest endpoint
79
+ │ ├── search/route.js # POST /api/search
80
+ │ └── ingest/route.js # POST /api/ingest
81
+ ├── components/
82
+ │ ├── ThemeRegistry.jsx # Light/dark mode with persistence
83
+ │ ├── Navbar.jsx # Branded AppBar + dark toggle
84
+ │ └── Footer.jsx # Powered-by footer
77
85
  ├── lib/
78
- │ ├── voyage.js # Voyage AI client
79
- │ ├── mongodb.js # MongoDB connection
80
- │ └── theme.js # MUI theme
86
+ │ ├── voyage.js # Voyage AI client
87
+ │ ├── mongodb.js # MongoDB connection + vector search
88
+ │ └── theme.js # MUI theme (light + dark palettes)
89
+ ├── public/favicon.svg
81
90
  ├── .env.example
82
- ├── package.json
83
- └── README.md
91
+ └── package.json
84
92
  ```
85
93
 
86
- ## API Endpoints
94
+ ---
95
+
96
+ ## 🌗 Features
97
+
98
+ - **Dark mode** — auto-detects system preference, persists choice
99
+ - **Score visualization** — color-coded progress bars for relevance scores
100
+ - **Copy to clipboard** — one-click copy on each result
101
+ - **Responsive** — works on mobile and desktop
102
+ - **Branded** — vai + Voyage AI + MongoDB branding throughout
103
+ {{#if rerank}}
104
+ - **Reranking** — results refined with {{rerankModel}} for better relevance
105
+ {{/if}}
106
+
107
+ ---
108
+
109
+ ## 🔌 API Endpoints
87
110
 
88
- ### POST /api/search
111
+ ### `POST /api/search`
89
112
 
90
113
  ```bash
91
114
  curl -X POST http://localhost:3000/api/search \
@@ -93,7 +116,7 @@ curl -X POST http://localhost:3000/api/search \
93
116
  -d '{"query": "How does vector search work?", "limit": 5}'
94
117
  ```
95
118
 
96
- ### POST /api/ingest
119
+ ### `POST /api/ingest`
97
120
 
98
121
  ```bash
99
122
  curl -X POST http://localhost:3000/api/ingest \
@@ -103,4 +126,4 @@ curl -X POST http://localhost:3000/api/ingest \
103
126
 
104
127
  ---
105
128
 
106
- Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}}
129
+ <sub>Generated by [vai](https://github.com/mrlynn/voyageai-cli) v{{vaiVersion}} on {{generatedAt}}</sub>
@@ -0,0 +1,11 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
2
+ <defs>
3
+ <linearGradient id="vg" x1="0" y1="0" x2="1" y2="1">
4
+ <stop offset="0%" stop-color="#00ED64"/>
5
+ <stop offset="100%" stop-color="#00D4AA"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="32" height="32" rx="8" fill="#001E2B"/>
9
+ <path d="M8 8 L16 26 L24 8" fill="none" stroke="url(#vg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
10
+ <text x="18" y="20" font-family="sans-serif" font-size="7" font-weight="bold" fill="url(#vg)">ai</text>
11
+ </svg>
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Branded footer
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ */
5
+
6
+ 'use client';
7
+
8
+ import { Box, Typography, Link, Stack, Divider } from '@mui/material';
9
+
10
+ export default function Footer() {
11
+ return (
12
+ <Box component="footer" sx={{ mt: 'auto', pt: 6, pb: 4 }}>
13
+ <Divider sx={{ mb: 3 }} />
14
+ <Stack
15
+ direction={{ xs: 'column', sm: 'row' }}
16
+ justifyContent="space-between"
17
+ alignItems="center"
18
+ spacing={1}
19
+ sx={{ px: 3 }}
20
+ >
21
+ <Typography variant="caption" color="text.secondary">
22
+ Built with{' '}
23
+ <Link
24
+ href="https://github.com/mrlynn/voyageai-cli"
25
+ target="_blank"
26
+ rel="noopener"
27
+ underline="hover"
28
+ color="primary"
29
+ fontWeight={600}
30
+ >
31
+ vai
32
+ </Link>
33
+ {' '}· Powered by{' '}
34
+ <Link href="https://www.voyageai.com" target="_blank" rel="noopener" underline="hover" color="inherit">
35
+ Voyage AI
36
+ </Link>
37
+ {' '}+{' '}
38
+ <Link href="https://www.mongodb.com/atlas/search" target="_blank" rel="noopener" underline="hover" color="inherit">
39
+ MongoDB Atlas Vector Search
40
+ </Link>
41
+ </Typography>
42
+
43
+ <Typography variant="caption" color="text.secondary" sx={{ fontFamily: 'monospace', fontSize: '0.65rem' }}>
44
+ v{{vaiVersion}}
45
+ </Typography>
46
+ </Stack>
47
+ </Box>
48
+ );
49
+ }
@@ -1,27 +1,33 @@
1
1
  /**
2
- * Root Layout with MUI Theme
2
+ * Root Layout with vai branding
3
3
  * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
4
  */
5
5
 
6
6
  import { AppRouterCacheProvider } from '@mui/material-nextjs/v14-appRouter';
7
- import { ThemeProvider } from '@mui/material/styles';
8
- import CssBaseline from '@mui/material/CssBaseline';
9
- import { theme } from '@/lib/theme';
7
+ import { ThemeRegistry } from '@/components/ThemeRegistry';
10
8
 
11
9
  export const metadata = {
12
- title: '{{projectName}}',
13
- description: 'Semantic search powered by Voyage AI',
10
+ title: '{{projectName}} — Semantic Search',
11
+ description: 'Semantic search powered by Voyage AI embeddings and MongoDB Atlas Vector Search.',
12
+ icons: { icon: '/favicon.svg' },
14
13
  };
15
14
 
16
15
  export default function RootLayout({ children }) {
17
16
  return (
18
- <html lang="en">
17
+ <html lang="en" suppressHydrationWarning>
18
+ <head>
19
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
20
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
21
+ <link
22
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
23
+ rel="stylesheet"
24
+ />
25
+ </head>
19
26
  <body>
20
27
  <AppRouterCacheProvider>
21
- <ThemeProvider theme={theme}>
22
- <CssBaseline />
28
+ <ThemeRegistry>
23
29
  {children}
24
- </ThemeProvider>
30
+ </ThemeRegistry>
25
31
  </AppRouterCacheProvider>
26
32
  </body>
27
33
  </html>
@@ -7,10 +7,10 @@
7
7
 
8
8
  import { MongoClient } from 'mongodb';
9
9
 
10
- const MONGODB_URI = process.env.MONGODB_URI;
11
-
12
- if (!MONGODB_URI) {
13
- throw new Error('MONGODB_URI environment variable is required');
10
+ function getUri() {
11
+ const uri = process.env.MONGODB_URI;
12
+ if (!uri) throw new Error('MONGODB_URI environment variable is required');
13
+ return uri;
14
14
  }
15
15
 
16
16
  // Cache the client promise for connection reuse in serverless
@@ -30,7 +30,7 @@ export async function getMongoClient() {
30
30
  }
31
31
 
32
32
  if (!cached.promise) {
33
- cached.promise = MongoClient.connect(MONGODB_URI);
33
+ cached.promise = MongoClient.connect(getUri());
34
34
  }
35
35
 
36
36
  cached.conn = await cached.promise;