voyageai-cli 1.24.0 → 1.26.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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +2 -0
  3. package/src/commands/about.js +1 -1
  4. package/src/commands/bug.js +1 -1
  5. package/src/commands/chat.js +281 -78
  6. package/src/commands/playground.js +73 -19
  7. package/src/commands/scaffold.js +23 -1
  8. package/src/commands/workflow.js +336 -0
  9. package/src/lib/chat.js +170 -4
  10. package/src/lib/explanations.js +53 -0
  11. package/src/lib/llm.js +304 -2
  12. package/src/lib/mongo.js +6 -6
  13. package/src/lib/prompt.js +60 -1
  14. package/src/lib/scaffold-structure.js +8 -9
  15. package/src/lib/telemetry.js +1 -1
  16. package/src/lib/template-engine.js +240 -0
  17. package/src/lib/templates/nextjs/README.md.tpl +78 -55
  18. package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
  19. package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
  20. package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
  21. package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
  22. package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
  23. package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
  24. package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
  25. package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
  26. package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
  27. package/src/lib/templates/nextjs/theme.js.tpl +138 -65
  28. package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
  29. package/src/lib/tool-registry.js +194 -0
  30. package/src/lib/workflow-utils.js +65 -0
  31. package/src/lib/workflow.js +1259 -0
  32. package/src/mcp/tools/embedding.js +55 -43
  33. package/src/mcp/tools/ingest.js +74 -67
  34. package/src/mcp/tools/management.js +54 -101
  35. package/src/mcp/tools/retrieval.js +181 -163
  36. package/src/mcp/tools/utility.js +171 -153
  37. package/src/playground/icons/dark/128.png +0 -0
  38. package/src/playground/icons/dark/16.png +0 -0
  39. package/src/playground/icons/dark/256.png +0 -0
  40. package/src/playground/icons/dark/32.png +0 -0
  41. package/src/playground/icons/dark/64.png +0 -0
  42. package/src/playground/icons/light/128.png +0 -0
  43. package/src/playground/icons/light/16.png +0 -0
  44. package/src/playground/icons/light/256.png +0 -0
  45. package/src/playground/icons/light/32.png +0 -0
  46. package/src/playground/icons/light/64.png +0 -0
  47. package/src/playground/icons/watermark.png +0 -0
  48. package/src/playground/index.html +633 -83
  49. package/src/workflows/consistency-check.json +64 -0
  50. package/src/workflows/cost-analysis.json +69 -0
  51. package/src/workflows/multi-collection-search.json +80 -0
  52. package/src/workflows/research-and-summarize.json +46 -0
  53. package/src/workflows/smart-ingest.json +63 -0
@@ -3,65 +3,77 @@
3
3
  const { generateEmbeddings } = require('../../lib/api');
4
4
  const { cosineSimilarity } = require('../../lib/math');
5
5
 
6
+ /**
7
+ * Handler for vai_embed: embed text and return the vector.
8
+ * @param {object} input - Validated input matching embedSchema
9
+ * @returns {Promise<{structuredContent: object, content: Array}>}
10
+ */
11
+ async function handleVaiEmbed(input) {
12
+ const embedOpts = { model: input.model, inputType: input.inputType };
13
+ if (input.dimensions) embedOpts.dimensions = input.dimensions;
14
+
15
+ const result = await generateEmbeddings([input.text], embedOpts);
16
+ const vector = result.data[0].embedding;
17
+
18
+ const structured = {
19
+ text: input.text.slice(0, 100) + (input.text.length > 100 ? '...' : ''),
20
+ model: input.model,
21
+ vector,
22
+ dimensions: vector.length,
23
+ inputType: input.inputType,
24
+ };
25
+
26
+ return {
27
+ structuredContent: structured,
28
+ content: [{ type: 'text', text: `Embedded text (${vector.length} dimensions, model: ${input.model}, type: ${input.inputType}). Vector: [${vector.slice(0, 5).map(v => v.toFixed(4)).join(', ')}, ... ${vector.length - 5} more]` }],
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Handler for vai_similarity: compare two texts semantically.
34
+ * @param {object} input - Validated input matching similaritySchema
35
+ * @returns {Promise<{structuredContent: object, content: Array}>}
36
+ */
37
+ async function handleVaiSimilarity(input) {
38
+ const result = await generateEmbeddings([input.text1, input.text2], {
39
+ model: input.model,
40
+ inputType: 'document',
41
+ });
42
+
43
+ const vec1 = result.data[0].embedding;
44
+ const vec2 = result.data[1].embedding;
45
+ const similarity = cosineSimilarity(vec1, vec2);
46
+
47
+ return {
48
+ structuredContent: {
49
+ text1: input.text1.slice(0, 100) + (input.text1.length > 100 ? '...' : ''),
50
+ text2: input.text2.slice(0, 100) + (input.text2.length > 100 ? '...' : ''),
51
+ similarity,
52
+ model: input.model,
53
+ },
54
+ content: [{ type: 'text', text: `Similarity: ${similarity.toFixed(4)} (model: ${input.model})\nText 1: "${input.text1.slice(0, 80)}..."\nText 2: "${input.text2.slice(0, 80)}..."` }],
55
+ };
56
+ }
57
+
6
58
  /**
7
59
  * Register embedding tools: vai_embed, vai_similarity
8
60
  * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
9
61
  * @param {object} schemas
10
62
  */
11
63
  function registerEmbeddingTools(server, schemas) {
12
- // vai_embed — embed text and return the vector
13
64
  server.tool(
14
65
  'vai_embed',
15
66
  'Embed text using a Voyage AI model and return the vector representation. Use when you need the raw embedding vector for custom similarity logic, storing in another system, or debugging.',
16
67
  schemas.embedSchema,
17
- async (input) => {
18
- const embedOpts = { model: input.model, inputType: input.inputType };
19
- if (input.dimensions) embedOpts.dimensions = input.dimensions;
20
-
21
- const result = await generateEmbeddings([input.text], embedOpts);
22
- const vector = result.data[0].embedding;
23
-
24
- const structured = {
25
- text: input.text.slice(0, 100) + (input.text.length > 100 ? '...' : ''),
26
- model: input.model,
27
- vector,
28
- dimensions: vector.length,
29
- inputType: input.inputType,
30
- };
31
-
32
- return {
33
- structuredContent: structured,
34
- content: [{ type: 'text', text: `Embedded text (${vector.length} dimensions, model: ${input.model}, type: ${input.inputType}). Vector: [${vector.slice(0, 5).map(v => v.toFixed(4)).join(', ')}, ... ${vector.length - 5} more]` }],
35
- };
36
- }
68
+ handleVaiEmbed
37
69
  );
38
70
 
39
- // vai_similarity — compare two texts
40
71
  server.tool(
41
72
  'vai_similarity',
42
73
  'Compare two texts semantically by embedding both and computing cosine similarity. Returns a score from -1 (opposite) to 1 (identical). Use for duplicate detection, relevance checking, or topic comparison.',
43
74
  schemas.similaritySchema,
44
- async (input) => {
45
- const result = await generateEmbeddings([input.text1, input.text2], {
46
- model: input.model,
47
- inputType: 'document',
48
- });
49
-
50
- const vec1 = result.data[0].embedding;
51
- const vec2 = result.data[1].embedding;
52
- const similarity = cosineSimilarity(vec1, vec2);
53
-
54
- return {
55
- structuredContent: {
56
- text1: input.text1.slice(0, 100) + (input.text1.length > 100 ? '...' : ''),
57
- text2: input.text2.slice(0, 100) + (input.text2.length > 100 ? '...' : ''),
58
- similarity,
59
- model: input.model,
60
- },
61
- content: [{ type: 'text', text: `Similarity: ${similarity.toFixed(4)} (model: ${input.model})\nText 1: "${input.text1.slice(0, 80)}..."\nText 2: "${input.text2.slice(0, 80)}..."` }],
62
- };
63
- }
75
+ handleVaiSimilarity
64
76
  );
65
77
  }
66
78
 
67
- module.exports = { registerEmbeddingTools };
79
+ module.exports = { registerEmbeddingTools, handleVaiEmbed, handleVaiSimilarity };
@@ -7,83 +7,90 @@ const { loadProject } = require('../../lib/project');
7
7
  const { getDefaultModel } = require('../../lib/catalog');
8
8
 
9
9
  /**
10
- * Register the vai_ingest tool (write operation).
11
- * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
12
- * @param {object} schemas
10
+ * Handler for vai_ingest: chunk, embed, and store a document.
11
+ * @param {object} input - Validated input matching ingestSchema
12
+ * @returns {Promise<{structuredContent: object, content: Array}>}
13
13
  */
14
- function registerIngestTool(server, schemas) {
15
- server.tool(
16
- 'vai_ingest',
17
- 'Add a document to a collection: chunks the text, embeds each chunk with Voyage AI, and stores them in MongoDB Atlas. Use when the user provides new content to add to the knowledge base.',
18
- schemas.ingestSchema,
19
- async (input) => {
20
- const { config: proj } = loadProject();
21
- const db = input.db || proj.db;
22
- const collName = input.collection || proj.collection;
23
- if (!db) throw new Error('No database specified. Pass db parameter or configure via vai init.');
24
- if (!collName) throw new Error('No collection specified. Pass collection parameter or configure via vai init.');
14
+ async function handleVaiIngest(input) {
15
+ const { config: proj } = loadProject();
16
+ const db = input.db || proj.db;
17
+ const collName = input.collection || proj.collection;
18
+ if (!db) throw new Error('No database specified. Pass db parameter or configure via vai init.');
19
+ if (!collName) throw new Error('No collection specified. Pass collection parameter or configure via vai init.');
25
20
 
26
- const model = input.model || proj.model || getDefaultModel();
27
- const start = Date.now();
21
+ const model = input.model || proj.model || getDefaultModel();
22
+ const start = Date.now();
28
23
 
29
- // Step 1: Chunk the text
30
- const chunks = chunk(input.text, {
31
- strategy: input.chunkStrategy,
32
- size: input.chunkSize,
33
- });
24
+ // Step 1: Chunk the text
25
+ const chunks = chunk(input.text, {
26
+ strategy: input.chunkStrategy,
27
+ size: input.chunkSize,
28
+ });
34
29
 
35
- if (chunks.length === 0) {
36
- return {
37
- structuredContent: { source: input.source || 'unknown', chunksCreated: 0, collection: collName },
38
- content: [{ type: 'text', text: 'No chunks produced — text may be too short or empty.' }],
39
- };
40
- }
30
+ if (chunks.length === 0) {
31
+ return {
32
+ structuredContent: { source: input.source || 'unknown', chunksCreated: 0, collection: collName },
33
+ content: [{ type: 'text', text: 'No chunks produced — text may be too short or empty.' }],
34
+ };
35
+ }
41
36
 
42
- // Step 2: Embed all chunks
43
- const embedResult = await generateEmbeddings(chunks, {
37
+ // Step 2: Embed all chunks
38
+ const embedResult = await generateEmbeddings(chunks, {
39
+ model,
40
+ inputType: 'document',
41
+ });
42
+
43
+ // Step 3: Store in MongoDB
44
+ const { client, collection: coll } = await getMongoCollection(db, collName);
45
+ try {
46
+ const docs = chunks.map((text, i) => ({
47
+ text,
48
+ embedding: embedResult.data[i].embedding,
49
+ source: input.source || 'mcp-ingest',
50
+ metadata: {
51
+ ...(input.metadata || {}),
52
+ ingestedAt: new Date().toISOString(),
53
+ chunkIndex: i,
54
+ totalChunks: chunks.length,
44
55
  model,
45
- inputType: 'document',
46
- });
56
+ chunkStrategy: input.chunkStrategy,
57
+ },
58
+ }));
47
59
 
48
- // Step 3: Store in MongoDB
49
- const { client, collection: coll } = await getMongoCollection(db, collName);
50
- try {
51
- const docs = chunks.map((text, i) => ({
52
- text,
53
- embedding: embedResult.data[i].embedding,
54
- source: input.source || 'mcp-ingest',
55
- metadata: {
56
- ...(input.metadata || {}),
57
- ingestedAt: new Date().toISOString(),
58
- chunkIndex: i,
59
- totalChunks: chunks.length,
60
- model,
61
- chunkStrategy: input.chunkStrategy,
62
- },
63
- }));
60
+ await coll.insertMany(docs);
61
+ const timeMs = Date.now() - start;
64
62
 
65
- await coll.insertMany(docs);
66
- const timeMs = Date.now() - start;
63
+ const structured = {
64
+ source: input.source || 'mcp-ingest',
65
+ chunksCreated: chunks.length,
66
+ collection: collName,
67
+ database: db,
68
+ model,
69
+ timeMs,
70
+ metadata: input.metadata || {},
71
+ };
67
72
 
68
- const structured = {
69
- source: input.source || 'mcp-ingest',
70
- chunksCreated: chunks.length,
71
- collection: collName,
72
- database: db,
73
- model,
74
- timeMs,
75
- metadata: input.metadata || {},
76
- };
73
+ return {
74
+ structuredContent: structured,
75
+ content: [{ type: 'text', text: `Ingested "${input.source || 'document'}" into ${db}.${collName}: ${chunks.length} chunks embedded with ${model} (${timeMs}ms)` }],
76
+ };
77
+ } finally {
78
+ await client.close();
79
+ }
80
+ }
77
81
 
78
- return {
79
- structuredContent: structured,
80
- content: [{ type: 'text', text: `Ingested "${input.source || 'document'}" into ${db}.${collName}: ${chunks.length} chunks embedded with ${model} (${timeMs}ms)` }],
81
- };
82
- } finally {
83
- await client.close();
84
- }
85
- }
82
+ /**
83
+ * Register the vai_ingest tool (write operation).
84
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
85
+ * @param {object} schemas
86
+ */
87
+ function registerIngestTool(server, schemas) {
88
+ server.tool(
89
+ 'vai_ingest',
90
+ 'Add a document to a collection: chunks the text, embeds each chunk with Voyage AI, and stores them in MongoDB Atlas. Use when the user provides new content to add to the knowledge base.',
91
+ schemas.ingestSchema,
92
+ handleVaiIngest
86
93
  );
87
94
  }
88
95
 
89
- module.exports = { registerIngestTool };
96
+ module.exports = { registerIngestTool, handleVaiIngest };
@@ -2,65 +2,63 @@
2
2
 
3
3
  const { MODEL_CATALOG } = require('../../lib/catalog');
4
4
  const { loadProject } = require('../../lib/project');
5
- const { requireMongoUri } = require('../../lib/mongo');
5
+ const { introspectCollections } = require('../../lib/workflow-utils');
6
6
 
7
7
  /**
8
- * Introspect MongoDB collections list collections with vector index info.
9
- * @param {string} dbName
10
- * @returns {Promise<Array<{ name: string, documentCount: number, hasVectorIndex: boolean, embeddingField?: string, dimensions?: number }>>}
8
+ * Handler for vai_collections: list collections with vector index info.
9
+ * @param {object} input - Validated input matching collectionsSchema
10
+ * @returns {Promise<{structuredContent: object, content: Array}>}
11
11
  */
12
- async function introspectCollections(dbName) {
13
- const { MongoClient } = require('mongodb');
14
- const uri = requireMongoUri();
15
- const client = new MongoClient(uri);
16
- await client.connect();
17
-
18
- try {
19
- const db = client.db(dbName);
20
- const collections = await db.listCollections().toArray();
21
- const results = [];
22
-
23
- for (const collInfo of collections) {
24
- if (collInfo.name.startsWith('system.')) continue;
25
- const coll = db.collection(collInfo.name);
26
- const documentCount = await coll.estimatedDocumentCount();
27
-
28
- let hasVectorIndex = false;
29
- let embeddingField;
30
- let dimensions;
31
-
32
- try {
33
- const indexes = await coll.listSearchIndexes().toArray();
34
- for (const idx of indexes) {
35
- // Atlas Search index definitions vary; look for vector type
36
- const fields = idx.latestDefinition?.fields || [];
37
- for (const f of fields) {
38
- if (f.type === 'vector') {
39
- hasVectorIndex = true;
40
- embeddingField = f.path;
41
- dimensions = f.numDimensions;
42
- break;
43
- }
44
- }
45
- if (hasVectorIndex) break;
46
- }
47
- } catch {
48
- // listSearchIndexes may not be available on non-Atlas deployments
49
- }
12
+ async function handleVaiCollections(input) {
13
+ const { config: proj } = loadProject();
14
+ const dbName = input.db || proj.db;
15
+ if (!dbName) throw new Error('No database specified. Pass db parameter or configure via vai init.');
16
+
17
+ const collections = await introspectCollections(dbName);
18
+
19
+ return {
20
+ structuredContent: { database: dbName, collections },
21
+ content: [{
22
+ type: 'text',
23
+ text: `Database: ${dbName}\n\n${collections.map(c =>
24
+ `• ${c.name} — ${c.documentCount} docs${c.hasVectorIndex ? ` ✓ vector index (${c.embeddingField}, ${c.dimensions}d)` : ''}`
25
+ ).join('\n')}`,
26
+ }],
27
+ };
28
+ }
50
29
 
51
- results.push({
52
- name: collInfo.name,
53
- documentCount,
54
- hasVectorIndex,
55
- ...(embeddingField && { embeddingField }),
56
- ...(dimensions && { dimensions }),
57
- });
58
- }
30
+ /**
31
+ * Handler for vai_models: list Voyage AI models.
32
+ * @param {object} input - Validated input matching modelsSchema
33
+ * @returns {Promise<{structuredContent: object, content: Array}>}
34
+ */
35
+ async function handleVaiModels(input) {
36
+ let models = MODEL_CATALOG.filter(m => !m.legacy && !m.unreleased);
59
37
 
60
- return results;
61
- } finally {
62
- await client.close();
38
+ if (input.category !== 'all') {
39
+ models = models.filter(m => m.type === input.category);
63
40
  }
41
+
42
+ const mapped = models.map(m => ({
43
+ id: m.name,
44
+ name: m.name,
45
+ type: m.type,
46
+ dimensions: m.dimensions,
47
+ maxTokens: m.maxTokens,
48
+ pricePerMToken: m.pricePerMToken,
49
+ ...(m.architecture && { architecture: m.architecture }),
50
+ ...(m.sharedSpace && { sharedSpace: m.sharedSpace }),
51
+ }));
52
+
53
+ return {
54
+ structuredContent: { category: input.category, models: mapped },
55
+ content: [{
56
+ type: 'text',
57
+ text: `Available ${input.category === 'all' ? '' : input.category + ' '}models:\n\n${mapped.map(m =>
58
+ `• ${m.name} (${m.type}) — ${m.dimensions}d, $${m.pricePerMToken}/M tokens`
59
+ ).join('\n')}`,
60
+ }],
61
+ };
64
62
  }
65
63
 
66
64
  /**
@@ -69,64 +67,19 @@ async function introspectCollections(dbName) {
69
67
  * @param {object} schemas
70
68
  */
71
69
  function registerManagementTools(server, schemas) {
72
- // vai_collections — list collections with vector index info
73
70
  server.tool(
74
71
  'vai_collections',
75
72
  'List available MongoDB collections with document counts and vector index information. Use at the start of a task to discover which knowledge bases exist, or when the user mentions a topic and you need to find the right collection.',
76
73
  schemas.collectionsSchema,
77
- async (input) => {
78
- const { config: proj } = loadProject();
79
- const dbName = input.db || proj.db;
80
- if (!dbName) throw new Error('No database specified. Pass db parameter or configure via vai init.');
81
-
82
- const collections = await introspectCollections(dbName);
83
-
84
- return {
85
- structuredContent: { database: dbName, collections },
86
- content: [{
87
- type: 'text',
88
- text: `Database: ${dbName}\n\n${collections.map(c =>
89
- `• ${c.name} — ${c.documentCount} docs${c.hasVectorIndex ? ` ✓ vector index (${c.embeddingField}, ${c.dimensions}d)` : ''}`
90
- ).join('\n')}`,
91
- }],
92
- };
93
- }
74
+ handleVaiCollections
94
75
  );
95
76
 
96
- // vai_models — list Voyage AI models
97
77
  server.tool(
98
78
  'vai_models',
99
79
  'List available Voyage AI models with capabilities, benchmarks, and pricing. Use when selecting a model for embedding or reranking, or when the user asks about model tradeoffs.',
100
80
  schemas.modelsSchema,
101
- async (input) => {
102
- let models = MODEL_CATALOG.filter(m => !m.legacy && !m.unreleased);
103
-
104
- if (input.category !== 'all') {
105
- models = models.filter(m => m.type === input.category);
106
- }
107
-
108
- const mapped = models.map(m => ({
109
- id: m.name,
110
- name: m.name,
111
- type: m.type,
112
- dimensions: m.dimensions,
113
- maxTokens: m.maxTokens,
114
- pricePerMToken: m.pricePerMToken,
115
- ...(m.architecture && { architecture: m.architecture }),
116
- ...(m.sharedSpace && { sharedSpace: m.sharedSpace }),
117
- }));
118
-
119
- return {
120
- structuredContent: { category: input.category, models: mapped },
121
- content: [{
122
- type: 'text',
123
- text: `Available ${input.category === 'all' ? '' : input.category + ' '}models:\n\n${mapped.map(m =>
124
- `• ${m.name} (${m.type}) — ${m.dimensions}d, $${m.pricePerMToken}/M tokens`
125
- ).join('\n')}`,
126
- }],
127
- };
128
- }
81
+ handleVaiModels
129
82
  );
130
83
  }
131
84
 
132
- module.exports = { registerManagementTools, introspectCollections };
85
+ module.exports = { registerManagementTools, handleVaiCollections, handleVaiModels, introspectCollections };