voyageai-cli 1.30.0 → 1.30.2

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 (82) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +8 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/chat.js +32 -11
  6. package/src/commands/code-search.js +751 -0
  7. package/src/commands/doctor.js +1 -1
  8. package/src/commands/export.js +124 -0
  9. package/src/commands/import.js +195 -0
  10. package/src/commands/index-workspace.js +243 -0
  11. package/src/commands/mcp-server.js +113 -3
  12. package/src/commands/playground.js +120 -4
  13. package/src/commands/quickstart.js +4 -4
  14. package/src/commands/workflow.js +132 -65
  15. package/src/lib/catalog.js +4 -2
  16. package/src/lib/code-search.js +315 -0
  17. package/src/lib/codegen.js +1 -1
  18. package/src/lib/explanations.js +3 -3
  19. package/src/lib/export/contexts/benchmark-export.js +27 -0
  20. package/src/lib/export/contexts/chat-export.js +41 -0
  21. package/src/lib/export/contexts/explore-export.js +22 -0
  22. package/src/lib/export/contexts/search-export.js +54 -0
  23. package/src/lib/export/contexts/workflow-export.js +80 -0
  24. package/src/lib/export/formats/clipboard-export.js +29 -0
  25. package/src/lib/export/formats/csv-export.js +45 -0
  26. package/src/lib/export/formats/json-export.js +50 -0
  27. package/src/lib/export/formats/markdown-export.js +189 -0
  28. package/src/lib/export/formats/mermaid-export.js +274 -0
  29. package/src/lib/export/formats/pdf-export.js +117 -0
  30. package/src/lib/export/formats/png-export.js +96 -0
  31. package/src/lib/export/formats/svg-export.js +116 -0
  32. package/src/lib/export/index.js +175 -0
  33. package/src/lib/github.js +226 -0
  34. package/src/lib/template-engine.js +154 -20
  35. package/src/lib/workflow-builder.js +753 -0
  36. package/src/lib/workflow-formatters.js +454 -0
  37. package/src/lib/workflow-input-cache.js +111 -0
  38. package/src/lib/workflow-scaffold.js +1 -1
  39. package/src/lib/workflow.js +297 -28
  40. package/src/mcp/install.js +280 -7
  41. package/src/mcp/schemas/index.js +170 -0
  42. package/src/mcp/server.js +19 -4
  43. package/src/mcp/tools/authoring.js +662 -0
  44. package/src/mcp/tools/code-search.js +620 -0
  45. package/src/mcp/tools/ingest.js +2 -5
  46. package/src/mcp/tools/retrieval.js +2 -15
  47. package/src/mcp/tools/workspace.js +452 -0
  48. package/src/mcp/utils.js +20 -0
  49. package/src/playground/announcements.md +52 -5
  50. package/src/playground/help/workflow-nodes.js +127 -2
  51. package/src/playground/index.html +17109 -12438
  52. package/src/playground/vendor/mermaid.min.js +2811 -0
  53. package/src/workflows/code-review.json +110 -0
  54. package/src/workflows/cost-analysis.json +5 -0
  55. package/src/workflows/rag-chat.json +165 -0
  56. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  57. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  58. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  59. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  60. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  61. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  62. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  63. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  64. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  65. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  66. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  67. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  68. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  69. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  70. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  71. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  72. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  73. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  74. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  75. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  76. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  77. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  78. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
  79. package/src/playground/assets/announcements/appstore.jpg +0 -0
  80. package/src/playground/assets/announcements/circuits.jpg +0 -0
  81. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  82. package/src/playground/assets/announcements/green-wave.jpg +0 -0
@@ -0,0 +1,662 @@
1
+ 'use strict';
2
+
3
+ const { validateWorkflow, buildExecutionPlan, buildDependencyGraph, ALL_TOOLS } = require('../../lib/workflow');
4
+
5
+ // ════════════════════════════════════════════════════════════════════
6
+ // Tool catalog: default inputs per tool
7
+ // ════════════════════════════════════════════════════════════════════
8
+
9
+ const TOOL_DEFAULTS = {
10
+ query: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' },
11
+ search: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' },
12
+ rerank: { query: '{{ inputs.query }}', documents: [], model: 'rerank-2.5' },
13
+ embed: { text: '{{ inputs.text }}' },
14
+ similarity: { text1: '{{ inputs.text1 }}', text2: '{{ inputs.text2 }}' },
15
+ ingest: { text: '{{ inputs.text }}', collection: '{{ inputs.collection }}' },
16
+ collections: {},
17
+ models: { category: 'all' },
18
+ explain: { topic: '{{ inputs.topic }}' },
19
+ estimate: { docs: '{{ inputs.docs }}', queries: '{{ inputs.queries }}', months: 12 },
20
+ generate: { prompt: '{{ inputs.prompt }}' },
21
+ template: { text: '' },
22
+ merge: { arrays: [], dedup: true },
23
+ filter: { array: [], condition: '' },
24
+ transform: { array: [], fields: [] },
25
+ conditional: { condition: '', then: [], else: [] },
26
+ loop: { items: [], as: 'item', step: {} },
27
+ chunk: { text: '{{ inputs.text }}', strategy: 'recursive', size: 512 },
28
+ aggregate: { pipeline: [] },
29
+ http: { url: '{{ inputs.url }}', method: 'GET' },
30
+ code_index: { source: '{{ inputs.source }}' },
31
+ code_search: { query: '{{ inputs.query }}' },
32
+ code_query: { query: '{{ inputs.query }}' },
33
+ code_find_similar: { code: '{{ inputs.code }}' },
34
+ code_status: {},
35
+ };
36
+
37
+ // ════════════════════════════════════════════════════════════════════
38
+ // Common workflow patterns
39
+ // ════════════════════════════════════════════════════════════════════
40
+
41
+ const PATTERNS = {
42
+ // Search then generate (RAG)
43
+ rag: {
44
+ tools: ['query', 'generate'],
45
+ inputs: {
46
+ query: { type: 'string', required: true, description: 'The question to answer' },
47
+ collection: { type: 'string', required: true, description: 'MongoDB collection with embedded documents' },
48
+ limit: { type: 'number', default: 5, description: 'Number of results to retrieve' },
49
+ },
50
+ steps: [
51
+ { id: 'retrieve', tool: 'query', name: 'Retrieve relevant documents', inputs: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' } },
52
+ { id: 'answer', tool: 'generate', name: 'Generate answer from context', inputs: { prompt: 'Answer the following question using the provided context.\n\nQuestion: {{ inputs.query }}', context: '{{ retrieve.output.results }}' } },
53
+ ],
54
+ output: { answer: '{{ answer.output.response }}', sources: '{{ retrieve.output.results }}' },
55
+ },
56
+
57
+ // Search + rerank
58
+ search_rerank: {
59
+ tools: ['search', 'rerank'],
60
+ inputs: {
61
+ query: { type: 'string', required: true, description: 'Search query' },
62
+ collection: { type: 'string', required: true, description: 'MongoDB collection' },
63
+ limit: { type: 'number', default: 10, description: 'Number of results' },
64
+ },
65
+ steps: [
66
+ { id: 'search_step', tool: 'search', name: 'Vector search', inputs: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: 50 } },
67
+ { id: 'rerank_step', tool: 'rerank', name: 'Rerank results', inputs: { query: '{{ inputs.query }}', documents: '{{ search_step.output.results }}', model: 'rerank-2.5' } },
68
+ ],
69
+ output: { results: '{{ rerank_step.output.results }}' },
70
+ },
71
+
72
+ // Ingest pipeline
73
+ ingest_pipeline: {
74
+ tools: ['chunk', 'ingest'],
75
+ inputs: {
76
+ text: { type: 'string', required: true, description: 'Text content to ingest' },
77
+ collection: { type: 'string', required: true, description: 'Target collection' },
78
+ source: { type: 'string', default: 'manual', description: 'Source identifier' },
79
+ },
80
+ steps: [
81
+ { id: 'chunk_step', tool: 'chunk', name: 'Chunk the text', inputs: { text: '{{ inputs.text }}', strategy: 'recursive', size: 512 } },
82
+ { id: 'ingest_step', tool: 'ingest', name: 'Embed and store chunks', inputs: { text: '{{ inputs.text }}', collection: '{{ inputs.collection }}', source: '{{ inputs.source }}' } },
83
+ ],
84
+ output: { chunks: '{{ chunk_step.output.totalChunks }}', inserted: '{{ ingest_step.output.insertedCount }}' },
85
+ },
86
+
87
+ // Compare embeddings
88
+ compare: {
89
+ tools: ['similarity'],
90
+ inputs: {
91
+ text1: { type: 'string', required: true, description: 'First text' },
92
+ text2: { type: 'string', required: true, description: 'Second text' },
93
+ },
94
+ steps: [
95
+ { id: 'compare_step', tool: 'similarity', name: 'Compare text similarity', inputs: { text1: '{{ inputs.text1 }}', text2: '{{ inputs.text2 }}' } },
96
+ ],
97
+ output: { similarity: '{{ compare_step.output.similarity }}' },
98
+ },
99
+
100
+ // Multi-search + merge + rerank
101
+ multi_search: {
102
+ tools: ['query', 'merge', 'rerank'],
103
+ inputs: {
104
+ query: { type: 'string', required: true, description: 'Search query' },
105
+ collection: { type: 'string', required: true, description: 'MongoDB collection' },
106
+ limit: { type: 'number', default: 10, description: 'Number of results' },
107
+ },
108
+ steps: [
109
+ { id: 'search_broad', tool: 'query', name: 'Broad search', inputs: { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: 20 } },
110
+ { id: 'merge_results', tool: 'merge', name: 'Merge results', inputs: { arrays: ['{{ search_broad.output.results }}'], dedup: true } },
111
+ { id: 'rerank_merged', tool: 'rerank', name: 'Rerank merged results', inputs: { query: '{{ inputs.query }}', documents: '{{ merge_results.output.results }}' } },
112
+ ],
113
+ output: { results: '{{ rerank_merged.output.results }}' },
114
+ },
115
+ };
116
+
117
+ // ════════════════════════════════════════════════════════════════════
118
+ // Pattern matching
119
+ // ════════════════════════════════════════════════════════════════════
120
+
121
+ /**
122
+ * Match a set of requested tools to the best known pattern.
123
+ * @param {string[]} tools
124
+ * @returns {string|null} pattern key or null
125
+ */
126
+ function matchPattern(tools) {
127
+ const toolSet = new Set(tools);
128
+
129
+ // Score each pattern by how many of its tools are present
130
+ let bestKey = null;
131
+ let bestScore = 0;
132
+
133
+ for (const [key, pattern] of Object.entries(PATTERNS)) {
134
+ const patternTools = new Set(pattern.tools);
135
+ let matches = 0;
136
+ for (const t of patternTools) {
137
+ if (toolSet.has(t)) matches++;
138
+ }
139
+ const score = matches / patternTools.size;
140
+ if (score > bestScore) {
141
+ bestScore = score;
142
+ bestKey = key;
143
+ }
144
+ }
145
+
146
+ return bestScore >= 0.5 ? bestKey : null;
147
+ }
148
+
149
+ // ════════════════════════════════════════════════════════════════════
150
+ // Workflow generation
151
+ // ════════════════════════════════════════════════════════════════════
152
+
153
+ /**
154
+ * Generate a workflow definition from a description and optional tools list.
155
+ *
156
+ * @param {object} params
157
+ * @param {string} params.description - Natural language description
158
+ * @param {string} [params.category] - Workflow category
159
+ * @param {string[]} [params.tools] - Explicit list of tools to use
160
+ * @returns {{ workflow: object, validation: object, executionPlan: string[][], dependencyGraph: object }}
161
+ */
162
+ function generateWorkflow({ description, category, tools }) {
163
+ // Determine tools from description if not provided
164
+ const requestedTools = tools && tools.length > 0
165
+ ? tools.filter(t => ALL_TOOLS.has(t))
166
+ : inferToolsFromDescription(description);
167
+
168
+ // Try to match a known pattern
169
+ const patternKey = matchPattern(requestedTools);
170
+ let workflow;
171
+
172
+ if (patternKey) {
173
+ workflow = buildFromPattern(patternKey, description, category, requestedTools);
174
+ } else {
175
+ workflow = buildFromToolList(description, category, requestedTools);
176
+ }
177
+
178
+ // Validate
179
+ const validationErrors = validateWorkflow(workflow);
180
+ const layers = validationErrors.length === 0
181
+ ? buildExecutionPlan(workflow.steps)
182
+ : [];
183
+
184
+ // Build dependency info
185
+ let dependencyGraph = {};
186
+ if (validationErrors.length === 0) {
187
+ const graph = buildDependencyGraph(workflow.steps);
188
+ for (const [stepId, deps] of graph) {
189
+ dependencyGraph[stepId] = [...deps];
190
+ }
191
+ }
192
+
193
+ return {
194
+ workflow,
195
+ validation: {
196
+ valid: validationErrors.length === 0,
197
+ errors: validationErrors,
198
+ },
199
+ executionPlan: layers,
200
+ dependencyGraph,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Infer tools from a natural language description.
206
+ * @param {string} description
207
+ * @returns {string[]}
208
+ */
209
+ function inferToolsFromDescription(description) {
210
+ const lower = description.toLowerCase();
211
+ const tools = [];
212
+
213
+ // Keywords to tool mapping
214
+ const keywords = {
215
+ query: ['search', 'query', 'find', 'retrieve', 'look up', 'lookup', 'rag', 'answer'],
216
+ generate: ['generate', 'summarize', 'answer', 'synthesize', 'write', 'compose', 'rag', 'explain answer'],
217
+ rerank: ['rerank', 're-rank', 'rank', 'sort by relevance', 'precision'],
218
+ embed: ['embed', 'embedding', 'vector', 'vectorize'],
219
+ similarity: ['similar', 'similarity', 'compare', 'distance'],
220
+ ingest: ['ingest', 'import', 'load', 'store', 'index document'],
221
+ chunk: ['chunk', 'split', 'segment', 'partition'],
222
+ merge: ['merge', 'combine', 'concat', 'join', 'union'],
223
+ filter: ['filter', 'exclude', 'include only', 'where'],
224
+ transform: ['transform', 'reshape', 'map', 'rename'],
225
+ template: ['template', 'format', 'compose text'],
226
+ conditional: ['conditional', 'if', 'branch', 'when'],
227
+ loop: ['loop', 'iterate', 'for each', 'batch'],
228
+ http: ['http', 'api', 'fetch', 'request', 'url', 'endpoint', 'webhook'],
229
+ estimate: ['cost', 'estimate', 'price', 'budget'],
230
+ models: ['model', 'models', 'list models', 'catalog'],
231
+ collections: ['collection', 'collections', 'list collections'],
232
+ aggregate: ['aggregate', 'pipeline', 'mongodb aggregate'],
233
+ search: ['full-text search', 'vector search'],
234
+ code_search: ['code search', 'search code', 'codebase search'],
235
+ code_index: ['code index', 'index code', 'index repo', 'index repository'],
236
+ code_query: ['code query', 'ask about code', 'codebase question'],
237
+ code_find_similar: ['find similar code', 'similar code', 'code clone'],
238
+ };
239
+
240
+ for (const [tool, kws] of Object.entries(keywords)) {
241
+ for (const kw of kws) {
242
+ if (lower.includes(kw)) {
243
+ tools.push(tool);
244
+ break;
245
+ }
246
+ }
247
+ }
248
+
249
+ // Default: if nothing matched, provide a basic query + generate (RAG) workflow
250
+ if (tools.length === 0) {
251
+ tools.push('query', 'generate');
252
+ }
253
+
254
+ // Deduplicate
255
+ return [...new Set(tools)];
256
+ }
257
+
258
+ /**
259
+ * Build a workflow from a known pattern.
260
+ */
261
+ function buildFromPattern(patternKey, description, category, requestedTools) {
262
+ const pattern = PATTERNS[patternKey];
263
+ const slug = slugify(description);
264
+
265
+ const workflow = {
266
+ name: slug,
267
+ description,
268
+ version: '1.0.0',
269
+ inputs: { ...pattern.inputs },
270
+ defaults: {},
271
+ steps: pattern.steps.map(s => ({ ...s, inputs: { ...s.inputs } })),
272
+ output: { ...pattern.output },
273
+ };
274
+
275
+ // Add any extra requested tools not in the pattern as additional steps
276
+ const patternTools = new Set(pattern.tools);
277
+ const extras = requestedTools.filter(t => !patternTools.has(t));
278
+
279
+ for (const tool of extras) {
280
+ const stepId = `${tool}_step`;
281
+ const step = buildStepForTool(tool, stepId, workflow);
282
+ workflow.steps.push(step);
283
+ }
284
+
285
+ if (category) {
286
+ workflow.category = category;
287
+ }
288
+
289
+ return workflow;
290
+ }
291
+
292
+ /**
293
+ * Build a workflow from an explicit tool list with no pattern match.
294
+ */
295
+ function buildFromToolList(description, category, requestedTools) {
296
+ const slug = slugify(description);
297
+
298
+ // Build inputs based on what tools need
299
+ const inputs = {};
300
+ const toolSet = new Set(requestedTools);
301
+
302
+ if (toolSet.has('query') || toolSet.has('search') || toolSet.has('code_search') || toolSet.has('code_query')) {
303
+ inputs.query = { type: 'string', required: true, description: 'Search query or question' };
304
+ }
305
+ if (toolSet.has('query') || toolSet.has('search') || toolSet.has('ingest')) {
306
+ inputs.collection = { type: 'string', required: true, description: 'MongoDB collection name' };
307
+ }
308
+ if (toolSet.has('query') || toolSet.has('search')) {
309
+ inputs.limit = { type: 'number', default: 10, description: 'Maximum results to return' };
310
+ }
311
+ if (toolSet.has('embed') || toolSet.has('chunk') || toolSet.has('ingest')) {
312
+ inputs.text = { type: 'string', required: true, description: 'Text content to process' };
313
+ }
314
+ if (toolSet.has('similarity')) {
315
+ inputs.text1 = { type: 'string', required: true, description: 'First text to compare' };
316
+ inputs.text2 = { type: 'string', required: true, description: 'Second text to compare' };
317
+ }
318
+ if (toolSet.has('http')) {
319
+ inputs.url = { type: 'string', required: true, description: 'URL for HTTP request' };
320
+ }
321
+ if (toolSet.has('estimate')) {
322
+ inputs.docs = { type: 'number', required: true, description: 'Number of documents' };
323
+ inputs.queries = { type: 'number', default: 0, description: 'Queries per month' };
324
+ }
325
+ if (toolSet.has('code_index')) {
326
+ inputs.source = { type: 'string', required: true, description: 'Path or URL of code to index' };
327
+ }
328
+ if (toolSet.has('code_find_similar')) {
329
+ inputs.code = { type: 'string', required: true, description: 'Code snippet to find similar implementations for' };
330
+ }
331
+ if (toolSet.has('explain')) {
332
+ inputs.topic = { type: 'string', required: true, description: 'Topic to explain' };
333
+ }
334
+
335
+ // Build steps in a sensible order
336
+ const steps = [];
337
+ const output = {};
338
+
339
+ // Data retrieval tools first
340
+ const orderedTools = orderTools(requestedTools);
341
+ let prevArrayStepId = null;
342
+
343
+ for (let i = 0; i < orderedTools.length; i++) {
344
+ const tool = orderedTools[i];
345
+ const stepId = orderedTools.filter((t, j) => j < i && t === tool).length > 0
346
+ ? `${tool}_step_${i}`
347
+ : `${tool}_step`;
348
+
349
+ const step = buildStepForTool(tool, stepId, { inputs, steps }, prevArrayStepId);
350
+ steps.push(step);
351
+
352
+ // Track steps that output arrays for chaining
353
+ if (['query', 'search', 'merge', 'filter', 'transform', 'rerank'].includes(tool)) {
354
+ prevArrayStepId = stepId;
355
+ }
356
+ }
357
+
358
+ // Build output from last step(s)
359
+ if (steps.length > 0) {
360
+ const lastStep = steps[steps.length - 1];
361
+ if (lastStep.tool === 'generate') {
362
+ output.response = `{{ ${lastStep.id}.output.response }}`;
363
+ } else if (['query', 'search', 'rerank', 'merge', 'filter', 'transform'].includes(lastStep.tool)) {
364
+ output.results = `{{ ${lastStep.id}.output.results }}`;
365
+ } else {
366
+ output.result = `{{ ${lastStep.id}.output }}`;
367
+ }
368
+ }
369
+
370
+ const workflow = {
371
+ name: slug,
372
+ description,
373
+ version: '1.0.0',
374
+ inputs,
375
+ defaults: {},
376
+ steps,
377
+ output,
378
+ };
379
+
380
+ if (category) {
381
+ workflow.category = category;
382
+ }
383
+
384
+ return workflow;
385
+ }
386
+
387
+ /**
388
+ * Build a single step definition for a given tool.
389
+ */
390
+ function buildStepForTool(tool, stepId, workflowContext, prevArrayStepId) {
391
+ const inputs = workflowContext.inputs || {};
392
+ const step = {
393
+ id: stepId,
394
+ tool,
395
+ name: humanizeTool(tool),
396
+ inputs: {},
397
+ };
398
+
399
+ switch (tool) {
400
+ case 'query':
401
+ step.inputs = { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' };
402
+ break;
403
+ case 'search':
404
+ step.inputs = { query: '{{ inputs.query }}', collection: '{{ inputs.collection }}', limit: '{{ inputs.limit }}' };
405
+ break;
406
+ case 'rerank':
407
+ step.inputs = {
408
+ query: '{{ inputs.query }}',
409
+ documents: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : '{{ inputs.documents }}',
410
+ model: 'rerank-2.5',
411
+ };
412
+ break;
413
+ case 'embed':
414
+ step.inputs = { text: '{{ inputs.text }}' };
415
+ break;
416
+ case 'similarity':
417
+ step.inputs = { text1: '{{ inputs.text1 }}', text2: '{{ inputs.text2 }}' };
418
+ break;
419
+ case 'ingest':
420
+ step.inputs = { text: '{{ inputs.text }}', collection: '{{ inputs.collection }}' };
421
+ break;
422
+ case 'chunk':
423
+ step.inputs = { text: '{{ inputs.text }}', strategy: 'recursive', size: 512 };
424
+ break;
425
+ case 'generate':
426
+ step.inputs = {
427
+ prompt: `Based on the provided context, ${workflowContext.inputs?.query ? 'answer the question: {{ inputs.query }}' : 'generate a response.'}`,
428
+ };
429
+ if (prevArrayStepId) {
430
+ step.inputs.context = `{{ ${prevArrayStepId}.output.results }}`;
431
+ }
432
+ break;
433
+ case 'merge':
434
+ step.inputs = { arrays: prevArrayStepId ? [`{{ ${prevArrayStepId}.output.results }}`] : [], dedup: true };
435
+ break;
436
+ case 'filter':
437
+ step.inputs = {
438
+ array: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : [],
439
+ condition: 'item.score > 0.5',
440
+ };
441
+ break;
442
+ case 'transform':
443
+ step.inputs = {
444
+ array: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : [],
445
+ fields: ['text', 'score'],
446
+ };
447
+ break;
448
+ case 'template':
449
+ step.inputs = { text: 'Workflow result summary' };
450
+ break;
451
+ case 'conditional':
452
+ step.inputs = { condition: 'true', then: [], else: [] };
453
+ break;
454
+ case 'loop':
455
+ step.inputs = {
456
+ items: prevArrayStepId ? `{{ ${prevArrayStepId}.output.results }}` : [],
457
+ as: 'doc',
458
+ step: { tool: 'embed', inputs: { text: '{{ doc.text }}' } },
459
+ };
460
+ break;
461
+ case 'http':
462
+ step.inputs = { url: '{{ inputs.url }}', method: 'GET' };
463
+ break;
464
+ case 'estimate':
465
+ step.inputs = { docs: '{{ inputs.docs }}', queries: '{{ inputs.queries }}', months: 12 };
466
+ break;
467
+ case 'models':
468
+ step.inputs = { category: 'all' };
469
+ break;
470
+ case 'collections':
471
+ step.inputs = {};
472
+ break;
473
+ case 'explain':
474
+ step.inputs = { topic: '{{ inputs.topic }}' };
475
+ break;
476
+ case 'aggregate':
477
+ step.inputs = { collection: '{{ inputs.collection }}', pipeline: [] };
478
+ break;
479
+ case 'code_index':
480
+ step.inputs = { source: '{{ inputs.source }}' };
481
+ break;
482
+ case 'code_search':
483
+ step.inputs = { query: '{{ inputs.query }}' };
484
+ break;
485
+ case 'code_query':
486
+ step.inputs = { query: '{{ inputs.query }}' };
487
+ break;
488
+ case 'code_find_similar':
489
+ step.inputs = { code: '{{ inputs.code }}' };
490
+ break;
491
+ case 'code_status':
492
+ step.inputs = {};
493
+ break;
494
+ default:
495
+ step.inputs = TOOL_DEFAULTS[tool] || {};
496
+ }
497
+
498
+ return step;
499
+ }
500
+
501
+ /**
502
+ * Order tools in a sensible execution sequence.
503
+ */
504
+ function orderTools(tools) {
505
+ const order = [
506
+ 'collections', 'models', 'code_status',
507
+ 'http',
508
+ 'chunk', 'code_index',
509
+ 'ingest',
510
+ 'embed',
511
+ 'query', 'search', 'code_search', 'code_query', 'code_find_similar',
512
+ 'similarity',
513
+ 'merge',
514
+ 'filter', 'transform',
515
+ 'rerank',
516
+ 'aggregate',
517
+ 'estimate', 'explain',
518
+ 'template',
519
+ 'conditional', 'loop',
520
+ 'generate',
521
+ ];
522
+
523
+ const orderMap = new Map(order.map((t, i) => [t, i]));
524
+ return [...tools].sort((a, b) => (orderMap.get(a) || 50) - (orderMap.get(b) || 50));
525
+ }
526
+
527
+ /**
528
+ * Create a human-readable name for a tool.
529
+ */
530
+ function humanizeTool(tool) {
531
+ const names = {
532
+ query: 'Query documents', search: 'Vector search', rerank: 'Rerank results',
533
+ embed: 'Generate embedding', similarity: 'Compare similarity',
534
+ ingest: 'Ingest documents', chunk: 'Chunk text', generate: 'Generate response',
535
+ merge: 'Merge results', filter: 'Filter results', transform: 'Transform data',
536
+ template: 'Compose text', conditional: 'Conditional branch', loop: 'Loop over items',
537
+ http: 'HTTP request', estimate: 'Cost estimate', models: 'List models',
538
+ collections: 'List collections', explain: 'Explain topic', aggregate: 'Aggregation pipeline',
539
+ code_index: 'Index codebase', code_search: 'Search code', code_query: 'Query codebase',
540
+ code_find_similar: 'Find similar code', code_status: 'Code index status',
541
+ };
542
+ return names[tool] || tool;
543
+ }
544
+
545
+ /**
546
+ * Slugify a description into a workflow name.
547
+ */
548
+ function slugify(text) {
549
+ return text
550
+ .toLowerCase()
551
+ .replace(/[^a-z0-9]+/g, '-')
552
+ .replace(/^-|-$/g, '')
553
+ .slice(0, 50);
554
+ }
555
+
556
+ // ════════════════════════════════════════════════════════════════════
557
+ // Validate workflow tool
558
+ // ════════════════════════════════════════════════════════════════════
559
+
560
+ /**
561
+ * Validate a workflow definition and return structured results.
562
+ *
563
+ * @param {object} params
564
+ * @param {object} params.workflow - The workflow JSON definition
565
+ * @returns {{ valid: boolean, errors: string[], warnings: string[], layers: string[][], dependencyGraph: object }}
566
+ */
567
+ function validateWorkflowTool({ workflow }) {
568
+ const errors = validateWorkflow(workflow);
569
+ let layers = [];
570
+ let dependencyGraph = {};
571
+
572
+ if (errors.length === 0 && Array.isArray(workflow.steps) && workflow.steps.length > 0) {
573
+ layers = buildExecutionPlan(workflow.steps);
574
+ const graph = buildDependencyGraph(workflow.steps);
575
+ for (const [stepId, deps] of graph) {
576
+ dependencyGraph[stepId] = [...deps];
577
+ }
578
+ }
579
+
580
+ // Separate warnings (non-blocking) from errors
581
+ // For now, validateWorkflow returns all as errors in strict mode
582
+ const warnings = [];
583
+
584
+ return {
585
+ valid: errors.length === 0,
586
+ errors,
587
+ warnings,
588
+ layers,
589
+ dependencyGraph,
590
+ };
591
+ }
592
+
593
+ // ════════════════════════════════════════════════════════════════════
594
+ // MCP Tool Registration
595
+ // ════════════════════════════════════════════════════════════════════
596
+
597
+ /**
598
+ * Handler for vai_generate_workflow MCP tool.
599
+ */
600
+ async function handleGenerateWorkflow(input) {
601
+ const result = generateWorkflow(input);
602
+
603
+ const summary = result.validation.valid
604
+ ? `Generated valid workflow "${result.workflow.name}" with ${result.workflow.steps.length} steps across ${result.executionPlan.length} execution layers.`
605
+ : `Generated workflow "${result.workflow.name}" with ${result.validation.errors.length} validation error(s). Review and fix the errors before running.`;
606
+
607
+ return {
608
+ structuredContent: result,
609
+ content: [{
610
+ type: 'text',
611
+ text: `${summary}\n\nWorkflow JSON:\n${JSON.stringify(result.workflow, null, 2)}\n\nExecution Plan: ${JSON.stringify(result.executionPlan)}\n\nValidation: ${result.validation.valid ? 'PASSED' : 'FAILED - ' + result.validation.errors.join('; ')}`,
612
+ }],
613
+ };
614
+ }
615
+
616
+ /**
617
+ * Handler for vai_validate_workflow MCP tool.
618
+ */
619
+ async function handleValidateWorkflow(input) {
620
+ const result = validateWorkflowTool(input);
621
+
622
+ const summary = result.valid
623
+ ? `Workflow is valid. ${result.layers.length} execution layer(s), ${Object.keys(result.dependencyGraph).length} step(s).`
624
+ : `Workflow has ${result.errors.length} error(s).`;
625
+
626
+ return {
627
+ structuredContent: result,
628
+ content: [{
629
+ type: 'text',
630
+ text: `${summary}\n\nErrors: ${result.errors.length > 0 ? result.errors.join('\n') : 'None'}\nWarnings: ${result.warnings.length > 0 ? result.warnings.join('\n') : 'None'}\nExecution Layers: ${JSON.stringify(result.layers)}\nDependency Graph: ${JSON.stringify(result.dependencyGraph, null, 2)}`,
631
+ }],
632
+ };
633
+ }
634
+
635
+ /**
636
+ * Register authoring tools on the MCP server.
637
+ * @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
638
+ * @param {object} schemas
639
+ */
640
+ function registerAuthoringTools(server, schemas) {
641
+ server.tool(
642
+ 'vai_generate_workflow',
643
+ 'Generate a complete, executable vai workflow JSON from a natural language description. Returns the workflow definition, validation results, and execution plan. The generated workflow uses template expressions for step inputs and follows all vai workflow conventions.',
644
+ schemas.generateWorkflowSchema,
645
+ handleGenerateWorkflow
646
+ );
647
+
648
+ server.tool(
649
+ 'vai_validate_workflow',
650
+ 'Validate a vai workflow JSON definition. Checks for structural errors, unknown tools, circular dependencies, and missing references. Returns validation errors, warnings, execution plan layers, and the dependency graph.',
651
+ schemas.validateWorkflowSchema,
652
+ handleValidateWorkflow
653
+ );
654
+ }
655
+
656
+ module.exports = {
657
+ registerAuthoringTools,
658
+ handleGenerateWorkflow,
659
+ handleValidateWorkflow,
660
+ generateWorkflow,
661
+ validateWorkflowTool,
662
+ };