voyageai-cli 1.23.1 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +1 -1
- package/src/commands/bug.js +1 -1
- package/src/commands/mcp-server.js +74 -0
- package/src/commands/playground.js +31 -0
- package/src/commands/scaffold.js +23 -1
- package/src/commands/workflow.js +336 -0
- package/src/lib/explanations.js +53 -0
- package/src/lib/scaffold-structure.js +8 -9
- package/src/lib/telemetry.js +1 -1
- package/src/lib/template-engine.js +240 -0
- package/src/lib/templates/nextjs/README.md.tpl +78 -55
- package/src/lib/templates/nextjs/favicon.svg.tpl +11 -0
- package/src/lib/templates/nextjs/footer.jsx.tpl +49 -0
- package/src/lib/templates/nextjs/layout.jsx.tpl +16 -10
- package/src/lib/templates/nextjs/lib-mongo.js.tpl +5 -5
- package/src/lib/templates/nextjs/lib-voyage.js.tpl +13 -8
- package/src/lib/templates/nextjs/navbar.jsx.tpl +98 -0
- package/src/lib/templates/nextjs/page-home.jsx.tpl +201 -0
- package/src/lib/templates/nextjs/page-search.jsx.tpl +184 -82
- package/src/lib/templates/nextjs/theme-registry.jsx.tpl +51 -0
- package/src/lib/templates/nextjs/theme.js.tpl +138 -65
- package/src/lib/templates/nextjs/vai-logo-256.png +0 -0
- package/src/lib/workflow-utils.js +65 -0
- package/src/lib/workflow.js +1259 -0
- package/src/mcp/install.js +201 -0
- package/src/mcp/tools/management.js +1 -60
- package/src/playground/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/icons/watermark.png +0 -0
- package/src/playground/index.html +125 -73
- package/src/workflows/consistency-check.json +64 -0
- package/src/workflows/cost-analysis.json +69 -0
- package/src/workflows/multi-collection-search.json +80 -0
- package/src/workflows/research-and-summarize.json +46 -0
- package/src/workflows/smart-ingest.json +63 -0
package/README.md
CHANGED
|
@@ -81,6 +81,7 @@ MongoDB LeafyGreen design system<br/><br/>
|
|
|
81
81
|
- [Environment & Auth](#environment--auth)
|
|
82
82
|
- [Shell Completions](#shell-completions)
|
|
83
83
|
- [All Commands](#all-commands)
|
|
84
|
+
- [MCP Server](#mcp-server)
|
|
84
85
|
- [Screenshots](#screenshots)
|
|
85
86
|
- [Requirements](#requirements)
|
|
86
87
|
- [Author](#author)
|
|
@@ -534,6 +535,12 @@ Covers all 22 commands, subcommands, flags, model names, and explain topics.
|
|
|
534
535
|
| `vai eval` | Evaluate retrieval quality (MRR, nDCG, Recall) |
|
|
535
536
|
| `vai eval compare` | Compare configurations side-by-side |
|
|
536
537
|
| `vai benchmark` | 8 subcommands for model comparison |
|
|
538
|
+
| **MCP Server** | |
|
|
539
|
+
| `vai mcp` | Start the MCP server (expose vai tools to AI agents) |
|
|
540
|
+
| `vai mcp install` | Install vai into AI tool configs (Claude, Cursor, etc.) |
|
|
541
|
+
| `vai mcp uninstall` | Remove vai from AI tool configs |
|
|
542
|
+
| `vai mcp status` | Show installation status across all tools |
|
|
543
|
+
| `vai mcp generate-key` | Generate API key for HTTP server auth |
|
|
537
544
|
| **Tools & Learning** | |
|
|
538
545
|
| `vai models` | List models, benchmarks, architecture |
|
|
539
546
|
| `vai explain` | 25 interactive concept explainers |
|
|
@@ -547,6 +554,63 @@ Covers all 22 commands, subcommands, flags, model names, and explain topics.
|
|
|
547
554
|
|
|
548
555
|
---
|
|
549
556
|
|
|
557
|
+
## MCP Server
|
|
558
|
+
|
|
559
|
+
Expose vai's RAG tools to any MCP-compatible AI agent — Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, and more. **11 tools** for embedding, retrieval, reranking, ingestion, and learning — all accessible without writing code.
|
|
560
|
+
|
|
561
|
+
### One-Command Setup
|
|
562
|
+
|
|
563
|
+
```bash
|
|
564
|
+
# Install into your AI tool of choice
|
|
565
|
+
vai mcp install claude
|
|
566
|
+
vai mcp install cursor
|
|
567
|
+
vai mcp install all # all supported tools at once
|
|
568
|
+
|
|
569
|
+
# Check what's configured
|
|
570
|
+
vai mcp status
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
The install command **merges** into existing configs — it won't touch your other MCP servers.
|
|
574
|
+
|
|
575
|
+
### Supported Tools
|
|
576
|
+
|
|
577
|
+
| Target | AI Tool |
|
|
578
|
+
|--------|---------|
|
|
579
|
+
| `claude` | Claude Desktop |
|
|
580
|
+
| `claude-code` | Claude Code |
|
|
581
|
+
| `cursor` | Cursor |
|
|
582
|
+
| `windsurf` | Windsurf |
|
|
583
|
+
| `vscode` | VS Code |
|
|
584
|
+
|
|
585
|
+
### What Your Agent Gets
|
|
586
|
+
|
|
587
|
+
Once installed, your AI agent can use these tools:
|
|
588
|
+
|
|
589
|
+
| Tool | What It Does |
|
|
590
|
+
|------|-------------|
|
|
591
|
+
| `vai_query` | Full RAG: embed → vector search → rerank |
|
|
592
|
+
| `vai_search` | Raw vector similarity search |
|
|
593
|
+
| `vai_rerank` | Rerank documents against a query |
|
|
594
|
+
| `vai_embed` | Generate embedding vectors |
|
|
595
|
+
| `vai_similarity` | Cosine similarity between texts |
|
|
596
|
+
| `vai_ingest` | Chunk, embed, and store documents |
|
|
597
|
+
| `vai_collections` | List MongoDB collections with vector indexes |
|
|
598
|
+
| `vai_models` | List models with pricing and benchmarks |
|
|
599
|
+
| `vai_topics` | Browse educational topics |
|
|
600
|
+
| `vai_explain` | Get detailed concept explanations |
|
|
601
|
+
| `vai_estimate` | Estimate embedding costs |
|
|
602
|
+
|
|
603
|
+
### Transport Modes
|
|
604
|
+
|
|
605
|
+
```bash
|
|
606
|
+
vai mcp # stdio (default, local)
|
|
607
|
+
vai mcp --transport http --port 3100 # HTTP (remote, multi-client)
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
📖 **Full documentation:** [docs/mcp-server.md](docs/mcp-server.md)
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
550
614
|
## Screenshots
|
|
551
615
|
|
|
552
616
|
### Desktop App — Dark Theme
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ const { register: registerQuickstart } = require('./commands/quickstart');
|
|
|
37
37
|
const { registerBug } = require('./commands/bug');
|
|
38
38
|
const { registerChat } = require('./commands/chat');
|
|
39
39
|
const { registerMcpServer } = require('./commands/mcp-server');
|
|
40
|
+
const { registerWorkflow } = require('./commands/workflow');
|
|
40
41
|
const { showBanner, showQuickStart, getVersion } = require('./lib/banner');
|
|
41
42
|
|
|
42
43
|
const version = getVersion();
|
|
@@ -78,6 +79,7 @@ registerQuickstart(program);
|
|
|
78
79
|
registerBug(program);
|
|
79
80
|
registerChat(program);
|
|
80
81
|
registerMcpServer(program);
|
|
82
|
+
registerWorkflow(program);
|
|
81
83
|
|
|
82
84
|
// Append disclaimer to all help output
|
|
83
85
|
program.addHelpText('after', `
|
package/src/commands/about.js
CHANGED
|
@@ -20,7 +20,7 @@ function registerAbout(program) {
|
|
|
20
20
|
name: 'Michael Lynn',
|
|
21
21
|
role: 'Principal Staff Developer Advocate, MongoDB',
|
|
22
22
|
github: 'https://github.com/mrlynn',
|
|
23
|
-
vai_website: 'https://
|
|
23
|
+
vai_website: 'https://vaicli.com',
|
|
24
24
|
website: 'https://mlynn.org',
|
|
25
25
|
},
|
|
26
26
|
links: {
|
package/src/commands/bug.js
CHANGED
|
@@ -15,7 +15,7 @@ function getVersion() {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const GITHUB_ISSUES_URL = 'https://github.com/mrlynn/voyageai-cli/issues/new';
|
|
18
|
-
const BUG_API_URL = 'https://
|
|
18
|
+
const BUG_API_URL = 'https://vaicli.com/api/bugs';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Generate a GitHub issue URL with pre-filled template
|
|
@@ -44,6 +44,80 @@ function registerMcpServer(program) {
|
|
|
44
44
|
const { generateKey } = require('../mcp/server');
|
|
45
45
|
generateKey();
|
|
46
46
|
});
|
|
47
|
+
|
|
48
|
+
// Subcommand: install
|
|
49
|
+
cmd
|
|
50
|
+
.command('install [targets...]')
|
|
51
|
+
.description('Install vai MCP server into AI tool configs (claude, claude-code, cursor, windsurf, vscode, or "all")')
|
|
52
|
+
.option('--force', 'Overwrite existing vai entry', false)
|
|
53
|
+
.option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
|
|
54
|
+
.option('--port <number>', 'HTTP port (http transport only)', (v) => parseInt(v, 10))
|
|
55
|
+
.option('--api-key <key>', 'Voyage API key to embed in config')
|
|
56
|
+
.action((targets, opts) => {
|
|
57
|
+
const { TARGETS, installTarget } = require('../mcp/install');
|
|
58
|
+
|
|
59
|
+
if (!targets.length) {
|
|
60
|
+
console.log('Usage: vai mcp install <target|all>');
|
|
61
|
+
console.log(`Available targets: ${Object.keys(TARGETS).join(', ')}, all`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const keys = targets.includes('all') ? Object.keys(TARGETS) : targets;
|
|
66
|
+
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
try {
|
|
69
|
+
const result = installTarget(key, {
|
|
70
|
+
force: opts.force,
|
|
71
|
+
transport: opts.transport,
|
|
72
|
+
port: opts.port,
|
|
73
|
+
apiKey: opts.apiKey,
|
|
74
|
+
});
|
|
75
|
+
console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(`❌ ${key}: ${err.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Subcommand: uninstall
|
|
83
|
+
cmd
|
|
84
|
+
.command('uninstall [targets...]')
|
|
85
|
+
.description('Remove vai MCP server from AI tool configs')
|
|
86
|
+
.action((targets) => {
|
|
87
|
+
const { TARGETS, uninstallTarget } = require('../mcp/install');
|
|
88
|
+
|
|
89
|
+
if (!targets.length) {
|
|
90
|
+
console.log('Usage: vai mcp uninstall <target|all>');
|
|
91
|
+
console.log(`Available targets: ${Object.keys(TARGETS).join(', ')}, all`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const keys = targets.includes('all') ? Object.keys(TARGETS) : targets;
|
|
96
|
+
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
try {
|
|
99
|
+
const result = uninstallTarget(key);
|
|
100
|
+
console.log(result.removed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error(`❌ ${key}: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Subcommand: status
|
|
108
|
+
cmd
|
|
109
|
+
.command('status')
|
|
110
|
+
.description('Show vai MCP installation status across all supported AI tools')
|
|
111
|
+
.action(() => {
|
|
112
|
+
const { statusAll } = require('../mcp/install');
|
|
113
|
+
const results = statusAll();
|
|
114
|
+
|
|
115
|
+
console.log('\nvai MCP Server — Installation Status\n');
|
|
116
|
+
for (const r of results) {
|
|
117
|
+
console.log(` ${r.status.padEnd(18)} ${r.name.padEnd(16)} ${r.configPath}`);
|
|
118
|
+
}
|
|
119
|
+
console.log('');
|
|
120
|
+
});
|
|
47
121
|
}
|
|
48
122
|
|
|
49
123
|
module.exports = { registerMcpServer };
|
|
@@ -85,6 +85,23 @@ function createPlaygroundServer() {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// Serve watermark image
|
|
89
|
+
if (req.method === 'GET' && req.url === '/icons/watermark.png') {
|
|
90
|
+
const wmPath = path.join(__dirname, '..', 'playground', 'icons', 'watermark.png');
|
|
91
|
+
if (fs.existsSync(wmPath)) {
|
|
92
|
+
const data = fs.readFileSync(wmPath);
|
|
93
|
+
res.writeHead(200, {
|
|
94
|
+
'Content-Type': 'image/png',
|
|
95
|
+
'Cache-Control': 'public, max-age=86400',
|
|
96
|
+
});
|
|
97
|
+
res.end(data);
|
|
98
|
+
} else {
|
|
99
|
+
res.writeHead(404);
|
|
100
|
+
res.end('Watermark not found');
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
// Serve icon assets: /icons/{dark|light}/{size}.png
|
|
89
106
|
const iconMatch = req.url.match(/^\/icons\/(dark|light)\/(\d+)\.png$/);
|
|
90
107
|
if (req.method === 'GET' && iconMatch) {
|
|
@@ -193,6 +210,20 @@ function createPlaygroundServer() {
|
|
|
193
210
|
}
|
|
194
211
|
}
|
|
195
212
|
|
|
213
|
+
// Add binary files
|
|
214
|
+
if (structure.binaryFiles) {
|
|
215
|
+
for (const file of structure.binaryFiles) {
|
|
216
|
+
const srcPath = path.join(__dirname, '..', 'lib', 'templates', target, file.source);
|
|
217
|
+
if (fs.existsSync(srcPath)) {
|
|
218
|
+
files.push({
|
|
219
|
+
name: `${projectName}/${file.output}`,
|
|
220
|
+
content: fs.readFileSync(srcPath),
|
|
221
|
+
binary: true,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
// Create ZIP
|
|
197
228
|
const zipBuffer = createZip(files);
|
|
198
229
|
|
package/src/commands/scaffold.js
CHANGED
|
@@ -129,6 +129,23 @@ function registerScaffold(program) {
|
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
+
|
|
133
|
+
// Add binary files (copied as-is from templates dir)
|
|
134
|
+
if (structure.binaryFiles) {
|
|
135
|
+
for (const file of structure.binaryFiles) {
|
|
136
|
+
const srcPath = path.join(__dirname, '..', 'lib', 'templates', target, file.source);
|
|
137
|
+
const destPath = path.join(projectDir, file.output);
|
|
138
|
+
const binarySize = fs.existsSync(srcPath) ? fs.statSync(srcPath).size : 0;
|
|
139
|
+
manifest.push({
|
|
140
|
+
path: file.output,
|
|
141
|
+
fullPath: destPath,
|
|
142
|
+
source: `${target}/${file.source}`,
|
|
143
|
+
size: binarySize,
|
|
144
|
+
binary: true,
|
|
145
|
+
binarySrc: srcPath,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
132
149
|
|
|
133
150
|
// JSON output mode
|
|
134
151
|
if (opts.json) {
|
|
@@ -171,7 +188,12 @@ function registerScaffold(program) {
|
|
|
171
188
|
|
|
172
189
|
// Write all files
|
|
173
190
|
for (const file of manifest) {
|
|
174
|
-
|
|
191
|
+
if (file.binary) {
|
|
192
|
+
ensureDir(path.dirname(file.fullPath));
|
|
193
|
+
fs.copyFileSync(file.binarySrc, file.fullPath);
|
|
194
|
+
} else {
|
|
195
|
+
writeFile(file.fullPath, file.content);
|
|
196
|
+
}
|
|
175
197
|
if (!opts.quiet) {
|
|
176
198
|
console.log(` ${ui.cyan('✓')} ${file.path}`);
|
|
177
199
|
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const pc = require('picocolors');
|
|
4
|
+
const ui = require('../lib/ui');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse repeatable --input key=value options into an object.
|
|
8
|
+
* Used as Commander's option reducer.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} pair - "key=value" string
|
|
11
|
+
* @param {object} prev - Accumulated object
|
|
12
|
+
* @returns {object}
|
|
13
|
+
*/
|
|
14
|
+
function collectInputs(pair, prev) {
|
|
15
|
+
const eq = pair.indexOf('=');
|
|
16
|
+
if (eq === -1) {
|
|
17
|
+
throw new Error(`Invalid input format: "${pair}". Expected key=value`);
|
|
18
|
+
}
|
|
19
|
+
const key = pair.slice(0, eq).trim();
|
|
20
|
+
const value = pair.slice(eq + 1).trim();
|
|
21
|
+
prev[key] = value;
|
|
22
|
+
return prev;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register the workflow command on a Commander program.
|
|
27
|
+
* @param {import('commander').Command} program
|
|
28
|
+
*/
|
|
29
|
+
function registerWorkflow(program) {
|
|
30
|
+
const wfCmd = program
|
|
31
|
+
.command('workflow')
|
|
32
|
+
.alias('wf')
|
|
33
|
+
.description('Run, validate, and manage composable RAG workflows');
|
|
34
|
+
|
|
35
|
+
// ── workflow run <file> ──
|
|
36
|
+
wfCmd
|
|
37
|
+
.command('run <file>')
|
|
38
|
+
.description('Execute a workflow file or built-in template')
|
|
39
|
+
.option('--input <key=value>', 'Set a workflow input (repeatable)', collectInputs, {})
|
|
40
|
+
.option('--db <name>', 'Override default database')
|
|
41
|
+
.option('--collection <name>', 'Override default collection')
|
|
42
|
+
.option('--json', 'Output results as JSON', false)
|
|
43
|
+
.option('--quiet', 'Suppress progress output', false)
|
|
44
|
+
.option('--dry-run', 'Show execution plan without running', false)
|
|
45
|
+
.option('--verbose', 'Show step details', false)
|
|
46
|
+
.action(async (file, opts) => {
|
|
47
|
+
const { loadWorkflow, executeWorkflow, buildExecutionPlan, validateWorkflow } = require('../lib/workflow');
|
|
48
|
+
|
|
49
|
+
let definition;
|
|
50
|
+
try {
|
|
51
|
+
definition = loadWorkflow(file);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(ui.error(err.message));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validation
|
|
58
|
+
const errors = validateWorkflow(definition);
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
console.error(ui.error('Workflow validation failed:'));
|
|
61
|
+
for (const e of errors) console.error(` ${pc.red('-')} ${e}`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const workflowName = definition.name || file;
|
|
66
|
+
|
|
67
|
+
if (opts.dryRun) {
|
|
68
|
+
// Dry run: show plan
|
|
69
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
70
|
+
const stepMap = new Map(definition.steps.map(s => [s.id, s]));
|
|
71
|
+
|
|
72
|
+
if (opts.json) {
|
|
73
|
+
console.log(JSON.stringify({ name: workflowName, layers, steps: definition.steps, inputs: opts.input }, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(`${pc.bold('vai workflow:')} ${workflowName} ${pc.dim('(dry run)')}`);
|
|
79
|
+
console.log(pc.dim('═'.repeat(50)));
|
|
80
|
+
console.log();
|
|
81
|
+
|
|
82
|
+
// Show inputs
|
|
83
|
+
const inputKeys = Object.keys(opts.input);
|
|
84
|
+
if (inputKeys.length > 0) {
|
|
85
|
+
console.log(pc.bold('Inputs:'));
|
|
86
|
+
for (const [k, v] of Object.entries(opts.input)) {
|
|
87
|
+
console.log(` ${pc.cyan(k)}: ${JSON.stringify(v)}`);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Show execution plan
|
|
93
|
+
console.log(pc.bold('Execution plan:'));
|
|
94
|
+
let stepNum = 1;
|
|
95
|
+
for (let i = 0; i < layers.length; i++) {
|
|
96
|
+
const layer = layers[i];
|
|
97
|
+
for (const stepId of layer) {
|
|
98
|
+
const step = stepMap.get(stepId);
|
|
99
|
+
const toolDesc = `${step.tool}(${summarizeInputs(step.inputs)})`;
|
|
100
|
+
console.log(` ${pc.dim(`${stepNum}.`)} ${pc.cyan(stepId)} ${pc.dim('->')} ${toolDesc}`);
|
|
101
|
+
stepNum++;
|
|
102
|
+
}
|
|
103
|
+
if (layer.length > 1) {
|
|
104
|
+
console.log(` ${pc.dim(`(${layer.join(' and ')} run in parallel)`)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Execute workflow
|
|
112
|
+
try {
|
|
113
|
+
const result = await executeWorkflow(definition, {
|
|
114
|
+
inputs: opts.input,
|
|
115
|
+
db: opts.db,
|
|
116
|
+
collection: opts.collection,
|
|
117
|
+
dryRun: false,
|
|
118
|
+
verbose: opts.verbose,
|
|
119
|
+
json: opts.json,
|
|
120
|
+
onStepStart: !opts.quiet ? (stepId, step) => {
|
|
121
|
+
process.stderr.write(` ${pc.dim('...')} ${step.name || stepId}\r`);
|
|
122
|
+
} : undefined,
|
|
123
|
+
onStepComplete: !opts.quiet ? (stepId, output, durationMs) => {
|
|
124
|
+
const stepDef = definition.steps.find(s => s.id === stepId);
|
|
125
|
+
const summary = summarizeOutput(stepDef?.tool, output);
|
|
126
|
+
console.error(` ${pc.green('✔')} ${stepDef?.name || stepId} ${pc.dim(summary)} ${pc.dim(`[${durationMs}ms]`)}`);
|
|
127
|
+
} : undefined,
|
|
128
|
+
onStepSkip: !opts.quiet ? (stepId, reason) => {
|
|
129
|
+
const stepDef = definition.steps.find(s => s.id === stepId);
|
|
130
|
+
console.error(` ${pc.yellow('⊘')} ${stepDef?.name || stepId} ${pc.dim(`skipped: ${reason}`)}`);
|
|
131
|
+
} : undefined,
|
|
132
|
+
onStepError: !opts.quiet ? (stepId, err) => {
|
|
133
|
+
const stepDef = definition.steps.find(s => s.id === stepId);
|
|
134
|
+
console.error(` ${pc.red('✗')} ${stepDef?.name || stepId} ${pc.red(err.message)}`);
|
|
135
|
+
} : undefined,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (!opts.quiet) {
|
|
139
|
+
console.error();
|
|
140
|
+
console.error(`${pc.bold('vai workflow:')} ${workflowName}`);
|
|
141
|
+
console.error(pc.dim('═'.repeat(50)));
|
|
142
|
+
// Step summaries already printed via callbacks
|
|
143
|
+
console.error();
|
|
144
|
+
console.error(`${pc.dim('Complete.')} ${result.steps.length} steps, ${result.totalTimeMs}ms total.`);
|
|
145
|
+
console.error();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Output
|
|
149
|
+
if (opts.json) {
|
|
150
|
+
console.log(JSON.stringify(result.output, null, 2));
|
|
151
|
+
} else if (result.output) {
|
|
152
|
+
// Pretty-print top results if they exist
|
|
153
|
+
const output = result.output;
|
|
154
|
+
if (output.results && Array.isArray(output.results)) {
|
|
155
|
+
const top = output.results.slice(0, 5);
|
|
156
|
+
console.log(pc.bold('Top results:'));
|
|
157
|
+
for (let i = 0; i < top.length; i++) {
|
|
158
|
+
const r = top[i];
|
|
159
|
+
const source = r.source || r.text?.slice(0, 50) || `result ${i + 1}`;
|
|
160
|
+
const score = r.score != null ? ` (${r.score.toFixed(2)})` : '';
|
|
161
|
+
console.log(` ${pc.dim(`[${i + 1}]`)} ${source}${pc.dim(score)}`);
|
|
162
|
+
}
|
|
163
|
+
} else if (output.summary) {
|
|
164
|
+
console.log(output.summary);
|
|
165
|
+
} else if (output.comparison) {
|
|
166
|
+
console.log(pc.bold('Cost comparison:'));
|
|
167
|
+
for (const item of output.comparison) {
|
|
168
|
+
if (item && item.model) {
|
|
169
|
+
console.log(` ${pc.cyan(item.model)}: $${item.totalCost} total (embed: $${item.embeddingCost}, queries: $${item.monthlyQueryCost}/mo)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
console.log(JSON.stringify(output, null, 2));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error(ui.error(err.message));
|
|
178
|
+
if (opts.verbose) {
|
|
179
|
+
console.error(pc.dim(err.stack));
|
|
180
|
+
}
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ── workflow validate <file> ──
|
|
186
|
+
wfCmd
|
|
187
|
+
.command('validate <file>')
|
|
188
|
+
.description('Validate a workflow file without executing')
|
|
189
|
+
.action((file) => {
|
|
190
|
+
const { loadWorkflow, validateWorkflow, buildExecutionPlan } = require('../lib/workflow');
|
|
191
|
+
|
|
192
|
+
let definition;
|
|
193
|
+
try {
|
|
194
|
+
definition = loadWorkflow(file);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error(ui.error(err.message));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const errors = validateWorkflow(definition);
|
|
201
|
+
if (errors.length > 0) {
|
|
202
|
+
console.error(ui.error(`Workflow has ${errors.length} error(s):`));
|
|
203
|
+
for (const e of errors) console.error(` ${pc.red('-')} ${e}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
208
|
+
console.log(ui.success(`${definition.name} is valid`));
|
|
209
|
+
console.log(` ${pc.dim('Steps:')} ${definition.steps.length}`);
|
|
210
|
+
console.log(` ${pc.dim('Layers:')} ${layers.length} (${layers.map(l => l.length).join(' + ')} steps)`);
|
|
211
|
+
|
|
212
|
+
if (definition.inputs) {
|
|
213
|
+
const required = Object.entries(definition.inputs).filter(([, s]) => s.required).map(([k]) => k);
|
|
214
|
+
if (required.length > 0) {
|
|
215
|
+
console.log(` ${pc.dim('Required inputs:')} ${required.join(', ')}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── workflow list ──
|
|
221
|
+
wfCmd
|
|
222
|
+
.command('list')
|
|
223
|
+
.description('List built-in workflow templates')
|
|
224
|
+
.action(() => {
|
|
225
|
+
const { listBuiltinWorkflows } = require('../lib/workflow');
|
|
226
|
+
|
|
227
|
+
const workflows = listBuiltinWorkflows();
|
|
228
|
+
if (workflows.length === 0) {
|
|
229
|
+
console.log(pc.dim('No built-in workflows found.'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log();
|
|
234
|
+
console.log(pc.bold('Built-in workflow templates:'));
|
|
235
|
+
console.log();
|
|
236
|
+
for (const wf of workflows) {
|
|
237
|
+
console.log(` ${pc.cyan(wf.name.padEnd(28))} ${wf.description}`);
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(pc.dim('Run with: vai workflow run <name> --input key=value'));
|
|
241
|
+
console.log();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ── workflow init ──
|
|
245
|
+
wfCmd
|
|
246
|
+
.command('init')
|
|
247
|
+
.description('Scaffold a new workflow file')
|
|
248
|
+
.option('--name <name>', 'Workflow name')
|
|
249
|
+
.option('--output <file>', 'Output file path')
|
|
250
|
+
.action((opts) => {
|
|
251
|
+
const fs = require('fs');
|
|
252
|
+
const name = opts.name || 'my-workflow';
|
|
253
|
+
const filename = opts.output || `./${name}.vai-workflow.json`;
|
|
254
|
+
|
|
255
|
+
const scaffold = {
|
|
256
|
+
$schema: 'https://vai.dev/schemas/workflow-v1.json',
|
|
257
|
+
name: name.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
|
|
258
|
+
description: 'TODO: describe what this workflow does',
|
|
259
|
+
version: '1.0.0',
|
|
260
|
+
inputs: {
|
|
261
|
+
query: {
|
|
262
|
+
type: 'string',
|
|
263
|
+
description: 'The search query',
|
|
264
|
+
required: true,
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
defaults: {},
|
|
268
|
+
steps: [
|
|
269
|
+
{
|
|
270
|
+
id: 'search',
|
|
271
|
+
name: 'Search knowledge base',
|
|
272
|
+
tool: 'query',
|
|
273
|
+
inputs: {
|
|
274
|
+
query: '{{ inputs.query }}',
|
|
275
|
+
limit: 10,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
output: {
|
|
280
|
+
results: '{{ search.output.results }}',
|
|
281
|
+
query: '{{ inputs.query }}',
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
fs.writeFileSync(filename, JSON.stringify(scaffold, null, 2) + '\n');
|
|
286
|
+
console.log(ui.success(`Created ${filename}`));
|
|
287
|
+
console.log(` ${pc.dim('Run with:')} vai workflow run ${filename} --input query="your question"`);
|
|
288
|
+
console.log(` ${pc.dim('Validate:')} vai workflow validate ${filename}`);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Summarize step inputs for dry-run display.
|
|
294
|
+
*/
|
|
295
|
+
function summarizeInputs(inputs) {
|
|
296
|
+
if (!inputs) return '';
|
|
297
|
+
const parts = [];
|
|
298
|
+
for (const [k, v] of Object.entries(inputs)) {
|
|
299
|
+
if (typeof v === 'string' && v.includes('{{')) {
|
|
300
|
+
parts.push(`${k}=<ref>`);
|
|
301
|
+
} else if (typeof v === 'string') {
|
|
302
|
+
parts.push(`${k}=${v.length > 20 ? v.slice(0, 20) + '...' : v}`);
|
|
303
|
+
} else if (typeof v === 'number' || typeof v === 'boolean') {
|
|
304
|
+
parts.push(`${k}=${v}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return parts.join(', ');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Summarize step output for display.
|
|
312
|
+
*/
|
|
313
|
+
function summarizeOutput(tool, output) {
|
|
314
|
+
if (!output) return '';
|
|
315
|
+
if (output.results && output.resultCount != null) {
|
|
316
|
+
return `${output.resultCount} results`;
|
|
317
|
+
}
|
|
318
|
+
if (output.insertedCount != null) {
|
|
319
|
+
return `${output.insertedCount} docs inserted`;
|
|
320
|
+
}
|
|
321
|
+
if (output.similarity != null) {
|
|
322
|
+
return `similarity: ${output.similarity.toFixed(4)}`;
|
|
323
|
+
}
|
|
324
|
+
if (output.text) {
|
|
325
|
+
return `${output.text.length} chars`;
|
|
326
|
+
}
|
|
327
|
+
if (output.embedding) {
|
|
328
|
+
return `${output.dimensions}d embedding`;
|
|
329
|
+
}
|
|
330
|
+
if (output.totalCost != null) {
|
|
331
|
+
return `$${output.totalCost}`;
|
|
332
|
+
}
|
|
333
|
+
return '';
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
module.exports = { registerWorkflow, collectInputs };
|