voyageai-cli 1.30.0 → 1.30.1
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/package.json +1 -1
- package/src/cli.js +6 -0
- package/src/commands/chat.js +32 -11
- package/src/commands/export.js +124 -0
- package/src/commands/import.js +195 -0
- package/src/commands/index-workspace.js +239 -0
- package/src/commands/mcp-server.js +113 -3
- package/src/commands/playground.js +111 -3
- package/src/lib/export/contexts/benchmark-export.js +27 -0
- package/src/lib/export/contexts/chat-export.js +41 -0
- package/src/lib/export/contexts/explore-export.js +22 -0
- package/src/lib/export/contexts/search-export.js +54 -0
- package/src/lib/export/contexts/workflow-export.js +80 -0
- package/src/lib/export/formats/clipboard-export.js +29 -0
- package/src/lib/export/formats/csv-export.js +45 -0
- package/src/lib/export/formats/json-export.js +50 -0
- package/src/lib/export/formats/markdown-export.js +189 -0
- package/src/lib/export/formats/mermaid-export.js +274 -0
- package/src/lib/export/formats/pdf-export.js +117 -0
- package/src/lib/export/formats/png-export.js +96 -0
- package/src/lib/export/formats/svg-export.js +116 -0
- package/src/lib/export/index.js +175 -0
- package/src/lib/workflow.js +206 -27
- package/src/mcp/install.js +280 -7
- package/src/mcp/schemas/index.js +40 -0
- package/src/mcp/server.js +2 -0
- package/src/mcp/tools/workspace.js +463 -0
- package/src/playground/announcements.md +52 -5
- package/src/playground/index.html +11125 -7796
- package/src/playground/vendor/mermaid.min.js +2811 -0
- package/src/workflows/rag-chat.json +165 -0
- package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
- package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
- package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
- package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
- package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
- package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
- package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
- package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
- package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
- package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
- package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
- package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
- package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
- package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
- package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
- package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
- package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
- package/src/playground/assets/announcements/appstore.jpg +0 -0
- package/src/playground/assets/announcements/circuits.jpg +0 -0
- package/src/playground/assets/announcements/csvingest.jpg +0 -0
- package/src/playground/assets/announcements/green-wave.jpg +0 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { generateEmbeddings } = require('../../lib/api');
|
|
6
|
+
const { getMongoCollection } = require('../../lib/mongo');
|
|
7
|
+
const { getDefaultModel } = require('../../lib/catalog');
|
|
8
|
+
const { chunk } = require('../../lib/chunker');
|
|
9
|
+
const { loadProject } = require('../../lib/project');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* File patterns for different content types.
|
|
13
|
+
*/
|
|
14
|
+
const FILE_PATTERNS = {
|
|
15
|
+
code: ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.rb', '.php', '.swift', '.kt', '.scala', '.ex', '.exs', '.clj', '.hs', '.ml', '.fs', '.vue', '.svelte'],
|
|
16
|
+
docs: ['.md', '.txt', '.rst', '.adoc', '.asciidoc', '.org', '.tex'],
|
|
17
|
+
config: ['.json', '.yaml', '.yml', '.toml', '.ini', '.env', '.conf'],
|
|
18
|
+
all: null, // Match everything except binary
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Files/directories to skip by default.
|
|
23
|
+
*/
|
|
24
|
+
const DEFAULT_IGNORE = [
|
|
25
|
+
'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out', 'target',
|
|
26
|
+
'__pycache__', '.cache', '.next', '.nuxt', 'coverage', '.nyc_output',
|
|
27
|
+
'vendor', 'venv', '.venv', 'env', '.env', '.idea', '.vscode',
|
|
28
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock',
|
|
29
|
+
'*.min.js', '*.min.css', '*.map', '*.chunk.js',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve db/collection from tool input, falling back to project config.
|
|
34
|
+
*/
|
|
35
|
+
function resolveDbCollection(input) {
|
|
36
|
+
const { config: proj } = loadProject();
|
|
37
|
+
const db = input.db || proj.db;
|
|
38
|
+
const collection = input.collection || proj.collection;
|
|
39
|
+
if (!db) throw new Error('No database specified. Pass db parameter or configure via vai init.');
|
|
40
|
+
if (!collection) throw new Error('No collection specified. Pass collection parameter or configure via vai init.');
|
|
41
|
+
return { db, collection };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Check if a path should be ignored.
|
|
46
|
+
*/
|
|
47
|
+
function shouldIgnore(filePath, ignorePatterns = DEFAULT_IGNORE) {
|
|
48
|
+
const basename = path.basename(filePath);
|
|
49
|
+
const relativePath = filePath;
|
|
50
|
+
|
|
51
|
+
for (const pattern of ignorePatterns) {
|
|
52
|
+
if (pattern.startsWith('*')) {
|
|
53
|
+
// Wildcard pattern (e.g., *.min.js)
|
|
54
|
+
const ext = pattern.slice(1);
|
|
55
|
+
if (basename.endsWith(ext)) return true;
|
|
56
|
+
} else if (relativePath.includes(pattern) || basename === pattern) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get file extension category.
|
|
66
|
+
*/
|
|
67
|
+
function getFileCategory(filePath) {
|
|
68
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
69
|
+
for (const [category, extensions] of Object.entries(FILE_PATTERNS)) {
|
|
70
|
+
if (extensions && extensions.includes(ext)) {
|
|
71
|
+
return category;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return 'other';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Recursively find files matching criteria.
|
|
79
|
+
*/
|
|
80
|
+
async function findFiles(dirPath, options = {}) {
|
|
81
|
+
const {
|
|
82
|
+
contentType = 'all',
|
|
83
|
+
ignorePatterns = DEFAULT_IGNORE,
|
|
84
|
+
maxFiles = 10000,
|
|
85
|
+
maxFileSize = 100000, // 100KB
|
|
86
|
+
} = options;
|
|
87
|
+
|
|
88
|
+
const files = [];
|
|
89
|
+
const extensions = FILE_PATTERNS[contentType];
|
|
90
|
+
|
|
91
|
+
async function walk(dir) {
|
|
92
|
+
if (files.length >= maxFiles) return;
|
|
93
|
+
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
97
|
+
} catch {
|
|
98
|
+
return; // Skip unreadable directories
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const entry of entries) {
|
|
102
|
+
if (files.length >= maxFiles) break;
|
|
103
|
+
|
|
104
|
+
const fullPath = path.join(dir, entry.name);
|
|
105
|
+
|
|
106
|
+
if (shouldIgnore(fullPath, ignorePatterns)) continue;
|
|
107
|
+
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
await walk(fullPath);
|
|
110
|
+
} else if (entry.isFile()) {
|
|
111
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
112
|
+
|
|
113
|
+
// Check extension match
|
|
114
|
+
if (extensions !== null && !extensions.includes(ext)) continue;
|
|
115
|
+
|
|
116
|
+
// Check file size
|
|
117
|
+
try {
|
|
118
|
+
const stats = await fs.promises.stat(fullPath);
|
|
119
|
+
if (stats.size > maxFileSize) continue;
|
|
120
|
+
if (stats.size === 0) continue;
|
|
121
|
+
} catch {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
files.push(fullPath);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await walk(dirPath);
|
|
131
|
+
return files;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Extract code metadata (functions, classes, etc.) from content.
|
|
136
|
+
*/
|
|
137
|
+
function extractCodeMetadata(content, filePath) {
|
|
138
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
139
|
+
const metadata = {
|
|
140
|
+
language: ext.slice(1),
|
|
141
|
+
lineCount: content.split('\n').length,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Simple extraction of function/class names for common languages
|
|
145
|
+
const patterns = {
|
|
146
|
+
js: [
|
|
147
|
+
/(?:function\s+|const\s+|let\s+|var\s+)(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\(|=>)|\()/g,
|
|
148
|
+
/class\s+(\w+)/g,
|
|
149
|
+
],
|
|
150
|
+
ts: [
|
|
151
|
+
/(?:function\s+|const\s+|let\s+)(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\(|=>)|[<(])/g,
|
|
152
|
+
/(?:class|interface|type)\s+(\w+)/g,
|
|
153
|
+
],
|
|
154
|
+
py: [
|
|
155
|
+
/(?:def|async def)\s+(\w+)\s*\(/g,
|
|
156
|
+
/class\s+(\w+)/g,
|
|
157
|
+
],
|
|
158
|
+
go: [
|
|
159
|
+
/func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/g,
|
|
160
|
+
/type\s+(\w+)\s+struct/g,
|
|
161
|
+
],
|
|
162
|
+
rs: [
|
|
163
|
+
/fn\s+(\w+)\s*[<(]/g,
|
|
164
|
+
/(?:struct|enum|trait)\s+(\w+)/g,
|
|
165
|
+
],
|
|
166
|
+
java: [
|
|
167
|
+
/(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(/g,
|
|
168
|
+
/class\s+(\w+)/g,
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const langPatterns = patterns[ext.slice(1)] || patterns.js;
|
|
173
|
+
const symbols = [];
|
|
174
|
+
|
|
175
|
+
for (const pattern of langPatterns) {
|
|
176
|
+
let match;
|
|
177
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
178
|
+
if (match[1] && !symbols.includes(match[1])) {
|
|
179
|
+
symbols.push(match[1]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (symbols.length > 0) {
|
|
185
|
+
metadata.symbols = symbols.slice(0, 50); // Limit to 50 symbols
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return metadata;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Handler for vai_index_workspace: index a workspace directory.
|
|
193
|
+
*/
|
|
194
|
+
async function handleIndexWorkspace(input) {
|
|
195
|
+
const { db, collection: collName } = resolveDbCollection(input);
|
|
196
|
+
const { config: proj } = loadProject();
|
|
197
|
+
const model = input.model || proj.model || getDefaultModel();
|
|
198
|
+
const workspacePath = input.path || process.cwd();
|
|
199
|
+
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
const stats = {
|
|
202
|
+
filesFound: 0,
|
|
203
|
+
filesIndexed: 0,
|
|
204
|
+
chunksCreated: 0,
|
|
205
|
+
errors: [],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Find files
|
|
209
|
+
const files = await findFiles(workspacePath, {
|
|
210
|
+
contentType: input.contentType || 'code',
|
|
211
|
+
maxFiles: input.maxFiles || 1000,
|
|
212
|
+
maxFileSize: input.maxFileSize || 100000,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
stats.filesFound = files.length;
|
|
216
|
+
|
|
217
|
+
if (files.length === 0) {
|
|
218
|
+
return {
|
|
219
|
+
structuredContent: { ...stats, timeMs: Date.now() - start },
|
|
220
|
+
content: [{ type: 'text', text: `No matching files found in ${workspacePath}` }],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Process files in batches
|
|
225
|
+
const batchSize = input.batchSize || 10;
|
|
226
|
+
const { client, collection } = await getMongoCollection(db, collName);
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
230
|
+
const batch = files.slice(i, i + batchSize);
|
|
231
|
+
const documents = [];
|
|
232
|
+
|
|
233
|
+
for (const filePath of batch) {
|
|
234
|
+
try {
|
|
235
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
236
|
+
const relativePath = path.relative(workspacePath, filePath);
|
|
237
|
+
const category = getFileCategory(filePath);
|
|
238
|
+
|
|
239
|
+
// Chunk the content
|
|
240
|
+
const chunkStrategy = category === 'code' ? 'recursive' : 'paragraph';
|
|
241
|
+
const chunks = chunk(content, {
|
|
242
|
+
strategy: chunkStrategy,
|
|
243
|
+
size: input.chunkSize || 512,
|
|
244
|
+
overlap: input.chunkOverlap || 50,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Create documents for each chunk
|
|
248
|
+
for (let j = 0; j < chunks.length; j++) {
|
|
249
|
+
const chunkText = chunks[j];
|
|
250
|
+
const metadata = {
|
|
251
|
+
source: relativePath,
|
|
252
|
+
filePath: filePath,
|
|
253
|
+
chunkIndex: j,
|
|
254
|
+
totalChunks: chunks.length,
|
|
255
|
+
category,
|
|
256
|
+
indexedAt: new Date().toISOString(),
|
|
257
|
+
...extractCodeMetadata(chunkText, filePath),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
documents.push({
|
|
261
|
+
text: chunkText,
|
|
262
|
+
metadata,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
stats.filesIndexed++;
|
|
267
|
+
} catch (err) {
|
|
268
|
+
stats.errors.push({ file: filePath, error: err.message });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Generate embeddings for batch
|
|
273
|
+
if (documents.length > 0) {
|
|
274
|
+
const texts = documents.map(d => d.text);
|
|
275
|
+
const embedResult = await generateEmbeddings(texts, { model, inputType: 'document' });
|
|
276
|
+
|
|
277
|
+
// Combine documents with embeddings and insert
|
|
278
|
+
const docsToInsert = documents.map((doc, idx) => ({
|
|
279
|
+
text: doc.text,
|
|
280
|
+
embedding: embedResult.data[idx].embedding,
|
|
281
|
+
metadata: doc.metadata,
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
await collection.insertMany(docsToInsert);
|
|
285
|
+
stats.chunksCreated += docsToInsert.length;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const timeMs = Date.now() - start;
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
structuredContent: {
|
|
293
|
+
...stats,
|
|
294
|
+
db,
|
|
295
|
+
collection: collName,
|
|
296
|
+
model,
|
|
297
|
+
timeMs,
|
|
298
|
+
},
|
|
299
|
+
content: [{
|
|
300
|
+
type: 'text',
|
|
301
|
+
text: `Indexed ${stats.filesIndexed}/${stats.filesFound} files (${stats.chunksCreated} chunks) in ${timeMs}ms\n` +
|
|
302
|
+
`Collection: ${db}.${collName}\n` +
|
|
303
|
+
(stats.errors.length > 0 ? `Errors: ${stats.errors.length}` : ''),
|
|
304
|
+
}],
|
|
305
|
+
};
|
|
306
|
+
} finally {
|
|
307
|
+
await client.close();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Handler for vai_search_code: semantic code search.
|
|
313
|
+
*/
|
|
314
|
+
async function handleSearchCode(input) {
|
|
315
|
+
const { db, collection: collName } = resolveDbCollection(input);
|
|
316
|
+
const { config: proj } = loadProject();
|
|
317
|
+
const model = input.model || proj.model || getDefaultModel();
|
|
318
|
+
const index = proj.index || 'vector_index';
|
|
319
|
+
const field = proj.field || 'embedding';
|
|
320
|
+
const start = Date.now();
|
|
321
|
+
|
|
322
|
+
// Embed query
|
|
323
|
+
const embedResult = await generateEmbeddings([input.query], { model, inputType: 'query' });
|
|
324
|
+
const queryVector = embedResult.data[0].embedding;
|
|
325
|
+
|
|
326
|
+
// Build filter
|
|
327
|
+
const filter = { ...input.filter };
|
|
328
|
+
if (input.language) {
|
|
329
|
+
filter['metadata.language'] = input.language;
|
|
330
|
+
}
|
|
331
|
+
if (input.category) {
|
|
332
|
+
filter['metadata.category'] = input.category;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Vector search
|
|
336
|
+
const { client, collection } = await getMongoCollection(db, collName);
|
|
337
|
+
try {
|
|
338
|
+
const vectorSearchStage = {
|
|
339
|
+
index,
|
|
340
|
+
path: field,
|
|
341
|
+
queryVector,
|
|
342
|
+
numCandidates: Math.min(input.limit * 15, 10000),
|
|
343
|
+
limit: input.limit,
|
|
344
|
+
};
|
|
345
|
+
if (Object.keys(filter).length > 0) {
|
|
346
|
+
vectorSearchStage.filter = filter;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const results = await collection.aggregate([
|
|
350
|
+
{ $vectorSearch: vectorSearchStage },
|
|
351
|
+
{ $addFields: { _vsScore: { $meta: 'vectorSearchScore' } } },
|
|
352
|
+
]).toArray();
|
|
353
|
+
|
|
354
|
+
const mapped = results.map(doc => ({
|
|
355
|
+
source: doc.metadata?.source || 'unknown',
|
|
356
|
+
filePath: doc.metadata?.filePath,
|
|
357
|
+
language: doc.metadata?.language,
|
|
358
|
+
content: doc.text || '',
|
|
359
|
+
score: doc._vsScore,
|
|
360
|
+
lineNumber: doc.metadata?.lineNumber,
|
|
361
|
+
symbols: doc.metadata?.symbols,
|
|
362
|
+
chunkIndex: doc.metadata?.chunkIndex,
|
|
363
|
+
}));
|
|
364
|
+
|
|
365
|
+
const timeMs = Date.now() - start;
|
|
366
|
+
|
|
367
|
+
// Format output
|
|
368
|
+
const lines = mapped.map((r, i) => {
|
|
369
|
+
let line = `[${i + 1}] ${r.source}`;
|
|
370
|
+
if (r.language) line += ` (${r.language})`;
|
|
371
|
+
line += ` — ${(r.score * 100).toFixed(1)}%`;
|
|
372
|
+
if (r.symbols?.length > 0) {
|
|
373
|
+
line += `\n Symbols: ${r.symbols.slice(0, 5).join(', ')}`;
|
|
374
|
+
}
|
|
375
|
+
line += `\n${r.content.slice(0, 300)}${r.content.length > 300 ? '...' : ''}`;
|
|
376
|
+
return line;
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
structuredContent: {
|
|
381
|
+
query: input.query,
|
|
382
|
+
results: mapped,
|
|
383
|
+
metadata: { collection: collName, model, timeMs, resultCount: mapped.length },
|
|
384
|
+
},
|
|
385
|
+
content: [{
|
|
386
|
+
type: 'text',
|
|
387
|
+
text: `Found ${mapped.length} code results for "${input.query}" (${timeMs}ms):\n\n${lines.join('\n\n')}`,
|
|
388
|
+
}],
|
|
389
|
+
};
|
|
390
|
+
} finally {
|
|
391
|
+
await client.close();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Handler for vai_explain_code: get contextual explanation for code.
|
|
397
|
+
*/
|
|
398
|
+
async function handleExplainCode(input) {
|
|
399
|
+
const { db, collection: collName } = resolveDbCollection(input);
|
|
400
|
+
const { config: proj } = loadProject();
|
|
401
|
+
const model = input.model || proj.model || getDefaultModel();
|
|
402
|
+
|
|
403
|
+
// Search for relevant context
|
|
404
|
+
const searchInput = {
|
|
405
|
+
query: `Explain: ${input.code.slice(0, 500)}`,
|
|
406
|
+
db,
|
|
407
|
+
collection: collName,
|
|
408
|
+
limit: input.contextLimit || 5,
|
|
409
|
+
language: input.language,
|
|
410
|
+
category: 'docs', // Prefer documentation for explanations
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const results = await handleSearchCode(searchInput);
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
structuredContent: {
|
|
417
|
+
code: input.code.slice(0, 200) + (input.code.length > 200 ? '...' : ''),
|
|
418
|
+
language: input.language,
|
|
419
|
+
context: results.structuredContent.results,
|
|
420
|
+
model,
|
|
421
|
+
},
|
|
422
|
+
content: [{
|
|
423
|
+
type: 'text',
|
|
424
|
+
text: `Context for code explanation:\n\n${results.content[0].text}`,
|
|
425
|
+
}],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Register workspace tools.
|
|
431
|
+
*/
|
|
432
|
+
function registerWorkspaceTools(server, schemas) {
|
|
433
|
+
server.tool(
|
|
434
|
+
'vai_index_workspace',
|
|
435
|
+
'Index a workspace/codebase for semantic code search. Recursively finds files, chunks content, generates embeddings, and stores in MongoDB. Use this to build a searchable knowledge base from a codebase.',
|
|
436
|
+
schemas.indexWorkspaceSchema,
|
|
437
|
+
handleIndexWorkspace
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
server.tool(
|
|
441
|
+
'vai_search_code',
|
|
442
|
+
'Semantic code search across an indexed codebase. Finds code snippets, functions, and documentation semantically related to your query. Use for understanding unfamiliar codebases or finding relevant code.',
|
|
443
|
+
schemas.searchCodeSchema,
|
|
444
|
+
handleSearchCode
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
server.tool(
|
|
448
|
+
'vai_explain_code',
|
|
449
|
+
'Get contextual explanation for code by finding relevant documentation and examples in the indexed knowledge base. Useful for understanding what code does or finding usage examples.',
|
|
450
|
+
schemas.explainCodeSchema,
|
|
451
|
+
handleExplainCode
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
module.exports = {
|
|
456
|
+
registerWorkspaceTools,
|
|
457
|
+
handleIndexWorkspace,
|
|
458
|
+
handleSearchCode,
|
|
459
|
+
handleExplainCode,
|
|
460
|
+
findFiles,
|
|
461
|
+
FILE_PATTERNS,
|
|
462
|
+
DEFAULT_IGNORE,
|
|
463
|
+
};
|
|
@@ -16,12 +16,60 @@ The title is the first `## ` heading, and the description is the paragraph below
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
id: ann-collapsible-layout
|
|
20
|
+
badge: New
|
|
21
|
+
published: 2026-02-15
|
|
22
|
+
expires: 2026-03-20
|
|
23
|
+
icon: 🖥️
|
|
24
|
+
bg_color: linear-gradient(135deg, #001E2B 0%, #0A2A3A 50%, rgba(0, 212, 170, 0.08) 100%)
|
|
25
|
+
cta_label: Try Workflows
|
|
26
|
+
cta_action: navigate
|
|
27
|
+
cta_target: /workflows
|
|
28
|
+
|
|
29
|
+
## Redesigned Workflows Layout
|
|
30
|
+
|
|
31
|
+
Maximize your canvas with collapsible panels, accordion properties, and library search. Collapse the sidebar to icon-only mode, toggle panels from the toolbar, or use keyboard shortcuts — Cmd+B, Cmd+I, Cmd+\\ for instant focus.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
id: ann-cost-dashboard
|
|
36
|
+
badge: New
|
|
37
|
+
published: 2026-02-15
|
|
38
|
+
expires: 2026-03-20
|
|
39
|
+
icon: 💰
|
|
40
|
+
bg_color: linear-gradient(135deg, #0A1E2B 0%, #112733 50%, rgba(64, 224, 255, 0.07) 100%)
|
|
41
|
+
cta_label: View Dashboard
|
|
42
|
+
cta_action: navigate
|
|
43
|
+
cta_target: /embed
|
|
44
|
+
|
|
45
|
+
## Live Cost Tracking Dashboard
|
|
46
|
+
|
|
47
|
+
Every embed, rerank, and tool call now shows real-time cost in the status bar. Expand the detail panel to see per-operation breakdowns, token counts, and cumulative spend across your session.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
id: ann-desktop-app
|
|
52
|
+
badge: Update
|
|
53
|
+
published: 2026-02-15
|
|
54
|
+
expires: 2026-04-01
|
|
55
|
+
icon: 🖥️
|
|
56
|
+
bg_color: linear-gradient(135deg, #001E2B 0%, #1C2D38 100%)
|
|
57
|
+
cta_label: Download
|
|
58
|
+
cta_action: link
|
|
59
|
+
cta_target: https://github.com/mrlynn/voyageai-cli/releases
|
|
60
|
+
|
|
61
|
+
## VAI Desktop App v1.30
|
|
62
|
+
|
|
63
|
+
The signed & notarized macOS desktop app is updated with the full collapsible layout, cost tracking, and all 22 Explore concepts including multimodal. Auto-updates built in.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
19
67
|
id: ann-voyage-4
|
|
20
68
|
badge: New Model
|
|
21
69
|
published: 2026-02-14
|
|
22
70
|
expires: 2026-03-15
|
|
23
71
|
icon: 🚀
|
|
24
|
-
|
|
72
|
+
bg_color: linear-gradient(135deg, rgba(0, 212, 170, 0.09) 0%, #001E2B 50%, rgba(64, 224, 255, 0.08) 100%)
|
|
25
73
|
cta_label: Try It Now
|
|
26
74
|
cta_action: navigate
|
|
27
75
|
cta_target: /benchmark
|
|
@@ -37,7 +85,7 @@ badge: New
|
|
|
37
85
|
published: 2026-02-12
|
|
38
86
|
expires: 2026-03-01
|
|
39
87
|
icon: 🏪
|
|
40
|
-
|
|
88
|
+
bg_color: linear-gradient(135deg, #112733 0%, #001E2B 100%)
|
|
41
89
|
cta_label: Explore Marketplace
|
|
42
90
|
cta_action: navigate
|
|
43
91
|
cta_target: /workflows
|
|
@@ -53,8 +101,7 @@ badge: Update
|
|
|
53
101
|
published: 2026-02-10
|
|
54
102
|
expires: 2026-04-01
|
|
55
103
|
icon: 📊
|
|
56
|
-
|
|
57
|
-
bg_color: linear-gradient(135deg, #1B5E20 0%, #2E7D32 100%)
|
|
104
|
+
bg_color: linear-gradient(135deg, #001E2B 0%, rgba(0, 212, 170, 0.06) 50%, #112733 100%)
|
|
58
105
|
cta_label: Learn More
|
|
59
106
|
cta_action: link
|
|
60
107
|
cta_target: https://docs.vaicli.com/csv-import
|
|
@@ -70,7 +117,7 @@ badge: New
|
|
|
70
117
|
published: 2026-02-13
|
|
71
118
|
expires: 2026-03-15
|
|
72
119
|
icon: ⚡
|
|
73
|
-
|
|
120
|
+
bg_color: linear-gradient(135deg, rgba(64, 224, 255, 0.06) 0%, #001E2B 50%, rgba(0, 212, 170, 0.06) 100%)
|
|
74
121
|
cta_label: Browse Store
|
|
75
122
|
cta_action: navigate
|
|
76
123
|
cta_target: /workflows
|