voyageai-cli 1.20.6 → 1.22.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 (42) hide show
  1. package/CHANGELOG.md +142 -26
  2. package/README.md +130 -2
  3. package/package.json +3 -2
  4. package/src/cli.js +10 -0
  5. package/src/commands/bug.js +249 -0
  6. package/src/commands/eval.js +420 -10
  7. package/src/commands/generate.js +220 -0
  8. package/src/commands/playground.js +93 -0
  9. package/src/commands/purge.js +271 -0
  10. package/src/commands/refresh.js +322 -0
  11. package/src/commands/scaffold.js +217 -0
  12. package/src/lib/codegen.js +339 -0
  13. package/src/lib/explanations.js +155 -0
  14. package/src/lib/scaffold-structure.js +114 -0
  15. package/src/lib/templates/nextjs/README.md.tpl +106 -0
  16. package/src/lib/templates/nextjs/env.example.tpl +8 -0
  17. package/src/lib/templates/nextjs/layout.jsx.tpl +29 -0
  18. package/src/lib/templates/nextjs/lib-mongo.js.tpl +111 -0
  19. package/src/lib/templates/nextjs/lib-voyage.js.tpl +103 -0
  20. package/src/lib/templates/nextjs/package.json.tpl +33 -0
  21. package/src/lib/templates/nextjs/page-search.jsx.tpl +147 -0
  22. package/src/lib/templates/nextjs/route-ingest.js.tpl +114 -0
  23. package/src/lib/templates/nextjs/route-search.js.tpl +97 -0
  24. package/src/lib/templates/nextjs/theme.js.tpl +84 -0
  25. package/src/lib/templates/python/README.md.tpl +145 -0
  26. package/src/lib/templates/python/app.py.tpl +221 -0
  27. package/src/lib/templates/python/chunker.py.tpl +127 -0
  28. package/src/lib/templates/python/env.example.tpl +12 -0
  29. package/src/lib/templates/python/mongo_client.py.tpl +125 -0
  30. package/src/lib/templates/python/requirements.txt.tpl +10 -0
  31. package/src/lib/templates/python/voyage_client.py.tpl +124 -0
  32. package/src/lib/templates/vanilla/README.md.tpl +156 -0
  33. package/src/lib/templates/vanilla/client.js.tpl +103 -0
  34. package/src/lib/templates/vanilla/connection.js.tpl +126 -0
  35. package/src/lib/templates/vanilla/env.example.tpl +11 -0
  36. package/src/lib/templates/vanilla/ingest.js.tpl +231 -0
  37. package/src/lib/templates/vanilla/package.json.tpl +31 -0
  38. package/src/lib/templates/vanilla/retrieval.js.tpl +100 -0
  39. package/src/lib/templates/vanilla/search-api.js.tpl +175 -0
  40. package/src/lib/templates/vanilla/server.js.tpl +81 -0
  41. package/src/lib/zip.js +130 -0
  42. package/src/playground/index.html +708 -3
@@ -0,0 +1,100 @@
1
+ /**
2
+ * RAG Retrieval Module
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * Model: {{model}}
6
+ * Database: {{db}}.{{collection}}
7
+ * Reranking: {{#if rerank}}enabled ({{rerankModel}}){{else}}disabled{{/if}}
8
+ */
9
+
10
+ const { embed{{#if rerank}}, rerank{{/if}} } = require('./client');
11
+ const { vectorSearch } = require('./connection');
12
+
13
+ /**
14
+ * Retrieve relevant documents for a query.
15
+ *
16
+ * Pipeline:
17
+ * 1. Embed the query using Voyage AI
18
+ * 2. Vector search in MongoDB Atlas
19
+ {{#if rerank}}
20
+ * 3. Rerank results using Voyage AI
21
+ {{/if}}
22
+ *
23
+ * @param {string} query - User's query
24
+ * @param {object} options - Retrieval options
25
+ * @param {number} options.limit - Final number of results (default: 5)
26
+ * @param {number} options.candidates - Initial candidates for reranking (default: 20)
27
+ * @param {object} options.filter - MongoDB pre-filter
28
+ * @returns {Promise<Array<{text: string, score: number, metadata: object}>>}
29
+ */
30
+ async function retrieve(query, options = {}) {
31
+ const limit = options.limit || 5;
32
+ {{#if rerank}}
33
+ const candidates = options.candidates || 20;
34
+ {{/if}}
35
+
36
+ // Step 1: Embed the query
37
+ const { embeddings } = await embed(query, { inputType: 'query' });
38
+ const queryEmbedding = embeddings[0];
39
+
40
+ // Step 2: Vector search
41
+ {{#if rerank}}
42
+ const searchResults = await vectorSearch(queryEmbedding, {
43
+ limit: candidates,
44
+ filter: options.filter,
45
+ });
46
+ {{else}}
47
+ const searchResults = await vectorSearch(queryEmbedding, {
48
+ limit,
49
+ filter: options.filter,
50
+ });
51
+ {{/if}}
52
+
53
+ if (searchResults.length === 0) {
54
+ return [];
55
+ }
56
+
57
+ {{#if rerank}}
58
+ // Step 3: Rerank for better relevance
59
+ const documents = searchResults.map(r => r.document.text);
60
+ const { results: reranked } = await rerank(query, documents, { topK: limit });
61
+
62
+ // Map reranked results back to full documents
63
+ return reranked.map(r => ({
64
+ text: searchResults[r.index].document.text,
65
+ score: r.relevanceScore,
66
+ metadata: searchResults[r.index].document.metadata,
67
+ }));
68
+ {{else}}
69
+ return searchResults.map(r => ({
70
+ text: r.document.text,
71
+ score: r.score,
72
+ metadata: r.document.metadata,
73
+ }));
74
+ {{/if}}
75
+ }
76
+
77
+ /**
78
+ * Format retrieved documents as context for an LLM prompt.
79
+ * @param {Array<{text: string, score: number}>} results - Retrieved documents
80
+ * @param {object} options - Formatting options
81
+ * @param {boolean} options.includeScores - Include relevance scores (default: false)
82
+ * @param {string} options.separator - Separator between documents (default: '\n\n---\n\n')
83
+ * @returns {string} Formatted context string
84
+ */
85
+ function formatContext(results, options = {}) {
86
+ const separator = options.separator || '\n\n---\n\n';
87
+
88
+ return results.map((r, i) => {
89
+ let text = r.text;
90
+ if (options.includeScores) {
91
+ text = `[Score: ${r.score.toFixed(3)}]\n${text}`;
92
+ }
93
+ return text;
94
+ }).join(separator);
95
+ }
96
+
97
+ module.exports = {
98
+ retrieve,
99
+ formatContext,
100
+ };
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Semantic Search API
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * Express router with endpoints:
6
+ * - POST /api/search - Semantic search
7
+ * - POST /api/ingest - Document ingestion
8
+ * - GET /api/health - Health check
9
+ */
10
+
11
+ const express = require('express');
12
+ const { retrieve, formatContext } = require('./retrieval');
13
+ const { ingestFile, ingestDirectory } = require('./ingest');
14
+ const { connect, close } = require('./connection');
15
+
16
+ const router = express.Router();
17
+
18
+ /**
19
+ * POST /api/search
20
+ *
21
+ * Request body:
22
+ * {
23
+ * "query": "What is vector search?",
24
+ * "limit": 5,
25
+ * "includeContext": true
26
+ * }
27
+ *
28
+ * Response:
29
+ * {
30
+ * "results": [...],
31
+ * "context": "...",
32
+ * "meta": { "model": "...", "took": 123 }
33
+ * }
34
+ */
35
+ router.post('/search', async (req, res) => {
36
+ const start = Date.now();
37
+
38
+ try {
39
+ const { query, limit = 5, includeContext = false, filter } = req.body;
40
+
41
+ if (!query || typeof query !== 'string') {
42
+ return res.status(400).json({ error: 'Query is required' });
43
+ }
44
+
45
+ const results = await retrieve(query, { limit, filter });
46
+
47
+ const response = {
48
+ results: results.map(r => ({
49
+ text: r.text,
50
+ score: r.score,
51
+ metadata: r.metadata,
52
+ })),
53
+ meta: {
54
+ model: '{{model}}',
55
+ {{#if rerank}}
56
+ rerankModel: '{{rerankModel}}',
57
+ {{/if}}
58
+ took: Date.now() - start,
59
+ },
60
+ };
61
+
62
+ if (includeContext) {
63
+ response.context = formatContext(results);
64
+ }
65
+
66
+ res.json(response);
67
+ } catch (err) {
68
+ console.error('Search error:', err);
69
+ res.status(500).json({ error: err.message });
70
+ }
71
+ });
72
+
73
+ /**
74
+ * POST /api/ingest
75
+ *
76
+ * Request body (for text):
77
+ * {
78
+ * "text": "Document content...",
79
+ * "metadata": { "source": "api", "title": "..." }
80
+ * }
81
+ *
82
+ * Request body (for file path):
83
+ * {
84
+ * "path": "./docs/guide.md"
85
+ * }
86
+ */
87
+ router.post('/ingest', async (req, res) => {
88
+ const start = Date.now();
89
+
90
+ try {
91
+ const { text, metadata, path: filePath } = req.body;
92
+
93
+ if (filePath) {
94
+ const result = await ingestFile(filePath);
95
+ return res.json({
96
+ success: true,
97
+ ...result,
98
+ took: Date.now() - start,
99
+ });
100
+ }
101
+
102
+ if (!text || typeof text !== 'string') {
103
+ return res.status(400).json({ error: 'Text or path is required' });
104
+ }
105
+
106
+ // For API-provided text, we need to chunk, embed, and store inline
107
+ const { embed } = require('./client');
108
+ const { insertDocuments } = require('./connection');
109
+ const { chunkText } = require('./ingest');
110
+
111
+ const chunks = chunkText(text);
112
+ const { embeddings, usage } = await embed(chunks, { inputType: 'document' });
113
+
114
+ const documents = chunks.map((chunk, i) => ({
115
+ text: chunk,
116
+ embedding: embeddings[i],
117
+ metadata: {
118
+ ...metadata,
119
+ source: 'api',
120
+ chunkIndex: i,
121
+ totalChunks: chunks.length,
122
+ },
123
+ }));
124
+
125
+ await insertDocuments(documents);
126
+
127
+ res.json({
128
+ success: true,
129
+ chunks: chunks.length,
130
+ tokens: usage.total_tokens,
131
+ took: Date.now() - start,
132
+ });
133
+ } catch (err) {
134
+ console.error('Ingest error:', err);
135
+ res.status(500).json({ error: err.message });
136
+ }
137
+ });
138
+
139
+ /**
140
+ * GET /api/health
141
+ */
142
+ router.get('/health', async (req, res) => {
143
+ try {
144
+ await connect();
145
+ res.json({
146
+ status: 'healthy',
147
+ model: '{{model}}',
148
+ database: '{{db}}',
149
+ collection: '{{collection}}',
150
+ });
151
+ } catch (err) {
152
+ res.status(503).json({
153
+ status: 'unhealthy',
154
+ error: err.message,
155
+ });
156
+ }
157
+ });
158
+
159
+ module.exports = router;
160
+
161
+ // Standalone server if run directly
162
+ if (require.main === module) {
163
+ const app = express();
164
+ app.use(express.json());
165
+ app.use('/api', router);
166
+
167
+ const PORT = process.env.PORT || 3000;
168
+
169
+ app.listen(PORT, () => {
170
+ console.log(`Search API running on http://localhost:${PORT}`);
171
+ console.log(` POST /api/search - Semantic search`);
172
+ console.log(` POST /api/ingest - Document ingestion`);
173
+ console.log(` GET /api/health - Health check`);
174
+ });
175
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Voyage AI RAG Server
3
+ * Generated by vai v{{vaiVersion}} on {{generatedAt}}
4
+ *
5
+ * Endpoints:
6
+ * - POST /api/search - Semantic search with optional reranking
7
+ * - POST /api/ingest - Document ingestion
8
+ * - GET /api/health - Health check
9
+ */
10
+
11
+ require('dotenv').config();
12
+
13
+ const express = require('express');
14
+ const searchRouter = require('./lib/search-api');
15
+
16
+ const app = express();
17
+ const PORT = process.env.PORT || 3000;
18
+
19
+ // Middleware
20
+ app.use(express.json({ limit: '10mb' }));
21
+
22
+ // CORS for development
23
+ app.use((req, res, next) => {
24
+ res.header('Access-Control-Allow-Origin', '*');
25
+ res.header('Access-Control-Allow-Headers', 'Content-Type');
26
+ res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
27
+ if (req.method === 'OPTIONS') return res.sendStatus(200);
28
+ next();
29
+ });
30
+
31
+ // API routes
32
+ app.use('/api', searchRouter);
33
+
34
+ // Root endpoint
35
+ app.get('/', (req, res) => {
36
+ res.json({
37
+ name: '{{projectName}}',
38
+ description: 'Voyage AI RAG API',
39
+ model: '{{model}}',
40
+ database: '{{db}}',
41
+ collection: '{{collection}}',
42
+ endpoints: {
43
+ search: 'POST /api/search',
44
+ ingest: 'POST /api/ingest',
45
+ health: 'GET /api/health',
46
+ },
47
+ });
48
+ });
49
+
50
+ // Error handler
51
+ app.use((err, req, res, next) => {
52
+ console.error('Server error:', err);
53
+ res.status(500).json({ error: 'Internal server error' });
54
+ });
55
+
56
+ app.listen(PORT, () => {
57
+ console.log(`
58
+ 🚀 Voyage AI RAG Server
59
+
60
+ Model: {{model}}
61
+ Database: {{db}}.{{collection}}
62
+ {{#if rerank}}
63
+ Reranking: {{rerankModel}}
64
+ {{/if}}
65
+
66
+ Endpoints:
67
+ POST /api/search - Semantic search
68
+ POST /api/ingest - Document ingestion
69
+ GET /api/health - Health check
70
+
71
+ Running on http://localhost:${PORT}
72
+ `);
73
+ });
74
+
75
+ // Graceful shutdown
76
+ process.on('SIGTERM', async () => {
77
+ console.log('Shutting down...');
78
+ const { close } = require('./lib/connection');
79
+ await close();
80
+ process.exit(0);
81
+ });
package/src/lib/zip.js ADDED
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Minimal ZIP file creator for text files.
5
+ * Creates uncompressed (STORE) ZIP archives - perfect for scaffolded code.
6
+ * No external dependencies.
7
+ */
8
+
9
+ /**
10
+ * Create a ZIP file from an array of file entries.
11
+ * @param {Array<{name: string, content: string}>} files - Files to include
12
+ * @returns {Buffer} - ZIP file as a Buffer
13
+ */
14
+ function createZip(files) {
15
+ const entries = [];
16
+ let offset = 0;
17
+
18
+ // Process each file
19
+ for (const file of files) {
20
+ const content = Buffer.from(file.content, 'utf8');
21
+ const name = Buffer.from(file.name, 'utf8');
22
+ const now = new Date();
23
+ const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xFFFF;
24
+ const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xFFFF;
25
+ const crc = crc32(content);
26
+
27
+ // Local file header (30 bytes + filename)
28
+ const localHeader = Buffer.alloc(30 + name.length);
29
+ localHeader.writeUInt32LE(0x04034b50, 0); // Local file header signature
30
+ localHeader.writeUInt16LE(20, 4); // Version needed (2.0)
31
+ localHeader.writeUInt16LE(0, 6); // General purpose bit flag
32
+ localHeader.writeUInt16LE(0, 8); // Compression method (0 = store)
33
+ localHeader.writeUInt16LE(dosTime, 10); // Last mod time
34
+ localHeader.writeUInt16LE(dosDate, 12); // Last mod date
35
+ localHeader.writeUInt32LE(crc, 14); // CRC-32
36
+ localHeader.writeUInt32LE(content.length, 18); // Compressed size
37
+ localHeader.writeUInt32LE(content.length, 22); // Uncompressed size
38
+ localHeader.writeUInt16LE(name.length, 26); // Filename length
39
+ localHeader.writeUInt16LE(0, 28); // Extra field length
40
+ name.copy(localHeader, 30);
41
+
42
+ entries.push({
43
+ name,
44
+ content,
45
+ crc,
46
+ dosTime,
47
+ dosDate,
48
+ headerOffset: offset,
49
+ localHeader,
50
+ });
51
+
52
+ offset += localHeader.length + content.length;
53
+ }
54
+
55
+ // Build central directory
56
+ const centralDir = [];
57
+ for (const entry of entries) {
58
+ const cdHeader = Buffer.alloc(46 + entry.name.length);
59
+ cdHeader.writeUInt32LE(0x02014b50, 0); // Central directory signature
60
+ cdHeader.writeUInt16LE(20, 4); // Version made by
61
+ cdHeader.writeUInt16LE(20, 6); // Version needed
62
+ cdHeader.writeUInt16LE(0, 8); // General purpose bit flag
63
+ cdHeader.writeUInt16LE(0, 10); // Compression method
64
+ cdHeader.writeUInt16LE(entry.dosTime, 12); // Last mod time
65
+ cdHeader.writeUInt16LE(entry.dosDate, 14); // Last mod date
66
+ cdHeader.writeUInt32LE(entry.crc, 16); // CRC-32
67
+ cdHeader.writeUInt32LE(entry.content.length, 20); // Compressed size
68
+ cdHeader.writeUInt32LE(entry.content.length, 24); // Uncompressed size
69
+ cdHeader.writeUInt16LE(entry.name.length, 28); // Filename length
70
+ cdHeader.writeUInt16LE(0, 30); // Extra field length
71
+ cdHeader.writeUInt16LE(0, 32); // Comment length
72
+ cdHeader.writeUInt16LE(0, 34); // Disk number start
73
+ cdHeader.writeUInt16LE(0, 36); // Internal file attributes
74
+ cdHeader.writeUInt32LE(0, 38); // External file attributes
75
+ cdHeader.writeUInt32LE(entry.headerOffset, 42); // Relative offset of local header
76
+ entry.name.copy(cdHeader, 46);
77
+ centralDir.push(cdHeader);
78
+ }
79
+
80
+ const centralDirBuffer = Buffer.concat(centralDir);
81
+ const centralDirOffset = offset;
82
+ const centralDirSize = centralDirBuffer.length;
83
+
84
+ // End of central directory record (22 bytes)
85
+ const eocd = Buffer.alloc(22);
86
+ eocd.writeUInt32LE(0x06054b50, 0); // EOCD signature
87
+ eocd.writeUInt16LE(0, 4); // Disk number
88
+ eocd.writeUInt16LE(0, 6); // Disk with central directory
89
+ eocd.writeUInt16LE(entries.length, 8); // Entries on this disk
90
+ eocd.writeUInt16LE(entries.length, 10); // Total entries
91
+ eocd.writeUInt32LE(centralDirSize, 12); // Central directory size
92
+ eocd.writeUInt32LE(centralDirOffset, 16); // Central directory offset
93
+ eocd.writeUInt16LE(0, 20); // Comment length
94
+
95
+ // Combine all parts
96
+ const parts = [];
97
+ for (const entry of entries) {
98
+ parts.push(entry.localHeader);
99
+ parts.push(entry.content);
100
+ }
101
+ parts.push(centralDirBuffer);
102
+ parts.push(eocd);
103
+
104
+ return Buffer.concat(parts);
105
+ }
106
+
107
+ /**
108
+ * CRC-32 calculation (IEEE polynomial).
109
+ */
110
+ const crcTable = (() => {
111
+ const table = new Uint32Array(256);
112
+ for (let i = 0; i < 256; i++) {
113
+ let c = i;
114
+ for (let j = 0; j < 8; j++) {
115
+ c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
116
+ }
117
+ table[i] = c;
118
+ }
119
+ return table;
120
+ })();
121
+
122
+ function crc32(buffer) {
123
+ let crc = 0xFFFFFFFF;
124
+ for (let i = 0; i < buffer.length; i++) {
125
+ crc = crcTable[(crc ^ buffer[i]) & 0xFF] ^ (crc >>> 8);
126
+ }
127
+ return (crc ^ 0xFFFFFFFF) >>> 0;
128
+ }
129
+
130
+ module.exports = { createZip };