voyageai-cli 1.30.1 → 1.30.3
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 +4 -4
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +3 -3
- package/src/commands/code-search.js +751 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/embed.js +121 -2
- package/src/commands/index-workspace.js +9 -5
- package/src/commands/playground.js +65 -4
- package/src/commands/quickstart.js +4 -4
- package/src/commands/workflow.js +132 -65
- package/src/lib/api.js +31 -0
- package/src/lib/catalog.js +4 -2
- package/src/lib/code-search.js +315 -0
- package/src/lib/codegen.js +1 -1
- package/src/lib/explanations.js +3 -3
- package/src/lib/github.js +226 -0
- package/src/lib/input.js +92 -1
- package/src/lib/template-engine.js +154 -20
- package/src/lib/workflow-builder.js +753 -0
- package/src/lib/workflow-formatters.js +454 -0
- package/src/lib/workflow-input-cache.js +111 -0
- package/src/lib/workflow-scaffold.js +1 -1
- package/src/lib/workflow.js +124 -8
- package/src/mcp/schemas/index.js +142 -0
- package/src/mcp/server.js +17 -4
- package/src/mcp/tools/authoring.js +662 -0
- package/src/mcp/tools/code-search.js +620 -0
- package/src/mcp/tools/embedding.js +72 -3
- package/src/mcp/tools/ingest.js +2 -5
- package/src/mcp/tools/retrieval.js +2 -15
- package/src/mcp/tools/workspace.js +1 -12
- package/src/mcp/utils.js +20 -0
- package/src/playground/help/workflow-nodes.js +127 -2
- package/src/playground/index.html +2013 -139
- package/src/workflows/code-review.json +110 -0
- package/src/workflows/cost-analysis.json +5 -0
- package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
- package/src/workflows/tests/code-review.happy-path.test.json +121 -0
- package/src/workflows/tests/code-review.no-question.test.json +70 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
package/src/commands/doctor.js
CHANGED
|
@@ -121,7 +121,7 @@ async function checkApiConnection() {
|
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
// Send minimal request body
|
|
124
|
-
req.write(JSON.stringify({ model: 'voyage-
|
|
124
|
+
req.write(JSON.stringify({ model: 'voyage-4-lite', input: ['test'] }));
|
|
125
125
|
req.end();
|
|
126
126
|
});
|
|
127
127
|
|
package/src/commands/embed.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { getDefaultModel } = require('../lib/catalog');
|
|
4
|
-
const { generateEmbeddings } = require('../lib/api');
|
|
5
|
-
const { resolveTextInput } = require('../lib/input');
|
|
4
|
+
const { generateEmbeddings, generateMultimodalEmbeddings } = require('../lib/api');
|
|
5
|
+
const { resolveTextInput, readMediaAsBase64, isImageFile, isVideoFile } = require('../lib/input');
|
|
6
6
|
const ui = require('../lib/ui');
|
|
7
7
|
const { showCostSummary } = require('../lib/cost-display');
|
|
8
8
|
|
|
9
|
+
const MULTIMODAL_MODEL = 'voyage-multimodal-3.5';
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Register the embed command on a Commander program.
|
|
11
13
|
* @param {import('commander').Command} program
|
|
@@ -18,6 +20,8 @@ function registerEmbed(program) {
|
|
|
18
20
|
.option('-t, --input-type <type>', 'Input type: query or document')
|
|
19
21
|
.option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
|
|
20
22
|
.option('-f, --file <path>', 'Read text from file')
|
|
23
|
+
.option('--image <path>', 'Embed an image file (uses voyage-multimodal-3.5)')
|
|
24
|
+
.option('--video <path>', 'Embed a video file (uses voyage-multimodal-3.5)')
|
|
21
25
|
.option('--truncation', 'Enable truncation for long inputs')
|
|
22
26
|
.option('--no-truncation', 'Disable truncation')
|
|
23
27
|
.option('--output-dtype <type>', 'Output data type: float, int8, uint8, binary, ubinary', 'float')
|
|
@@ -28,6 +32,121 @@ function registerEmbed(program) {
|
|
|
28
32
|
.action(async (text, opts) => {
|
|
29
33
|
try {
|
|
30
34
|
const telemetry = require('../lib/telemetry');
|
|
35
|
+
const isMultimodal = !!(opts.image || opts.video);
|
|
36
|
+
|
|
37
|
+
// Validate: --image/--video are incompatible with --file
|
|
38
|
+
if (isMultimodal && opts.file) {
|
|
39
|
+
console.error(ui.error('Cannot combine --image or --video with --file. Use --image/--video for multimodal, or --file for text.'));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Multimodal path: --image and/or --video
|
|
44
|
+
if (isMultimodal) {
|
|
45
|
+
const model = opts.model === getDefaultModel() ? MULTIMODAL_MODEL : opts.model;
|
|
46
|
+
const useColor = !opts.json;
|
|
47
|
+
const useSpinner = useColor && !opts.quiet;
|
|
48
|
+
|
|
49
|
+
// Build content array
|
|
50
|
+
const contentItems = [];
|
|
51
|
+
const mediaMeta = [];
|
|
52
|
+
|
|
53
|
+
// Add text if provided
|
|
54
|
+
if (text) {
|
|
55
|
+
contentItems.push({ type: 'text', text });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Add image
|
|
59
|
+
if (opts.image) {
|
|
60
|
+
if (!isImageFile(opts.image)) {
|
|
61
|
+
console.error(ui.error(`Not a supported image format: ${opts.image}`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const media = readMediaAsBase64(opts.image);
|
|
65
|
+
contentItems.push({ type: 'image_base64', image_base64: media.base64DataUrl });
|
|
66
|
+
mediaMeta.push({ type: 'image', path: opts.image, mime: media.mimeType, size: media.sizeBytes });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Add video
|
|
70
|
+
if (opts.video) {
|
|
71
|
+
if (!isVideoFile(opts.video)) {
|
|
72
|
+
console.error(ui.error(`Not a supported video format: ${opts.video}`));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const media = readMediaAsBase64(opts.video);
|
|
76
|
+
contentItems.push({ type: 'video_base64', video_base64: media.base64DataUrl });
|
|
77
|
+
mediaMeta.push({ type: 'video', path: opts.video, mime: media.mimeType, size: media.sizeBytes });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (contentItems.length === 0) {
|
|
81
|
+
console.error(ui.error('No content provided. Pass text, --image, or --video.'));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const done = telemetry.timer('cli_embed', {
|
|
86
|
+
model,
|
|
87
|
+
multimodal: true,
|
|
88
|
+
hasText: !!text,
|
|
89
|
+
hasImage: !!opts.image,
|
|
90
|
+
hasVideo: !!opts.video,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
let spin;
|
|
94
|
+
if (useSpinner) {
|
|
95
|
+
spin = ui.spinner('Generating multimodal embeddings...');
|
|
96
|
+
spin.start();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const mmOpts = { model };
|
|
100
|
+
if (opts.inputType) mmOpts.inputType = opts.inputType;
|
|
101
|
+
if (opts.dimensions) mmOpts.outputDimension = opts.dimensions;
|
|
102
|
+
|
|
103
|
+
const result = await generateMultimodalEmbeddings([contentItems], mmOpts);
|
|
104
|
+
|
|
105
|
+
if (spin) spin.stop();
|
|
106
|
+
|
|
107
|
+
if (opts.outputFormat === 'array') {
|
|
108
|
+
console.log(JSON.stringify(result.data[0].embedding));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
console.log(JSON.stringify(result, null, 2));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Friendly output
|
|
118
|
+
if (!opts.quiet) {
|
|
119
|
+
console.log(ui.label('Model', ui.cyan(model)));
|
|
120
|
+
console.log(ui.label('Mode', ui.cyan('multimodal')));
|
|
121
|
+
for (const m of mediaMeta) {
|
|
122
|
+
const sizeStr = m.size < 1024 * 1024
|
|
123
|
+
? `${(m.size / 1024).toFixed(1)} KB`
|
|
124
|
+
: `${(m.size / (1024 * 1024)).toFixed(1)} MB`;
|
|
125
|
+
console.log(ui.label(m.type === 'image' ? 'Image' : 'Video', `${m.path} ${ui.dim(`(${m.mime}, ${sizeStr})`)}`));
|
|
126
|
+
}
|
|
127
|
+
if (text) {
|
|
128
|
+
console.log(ui.label('Text', ui.dim(text.slice(0, 80) + (text.length > 80 ? '...' : ''))));
|
|
129
|
+
}
|
|
130
|
+
if (result.usage) {
|
|
131
|
+
console.log(ui.label('Tokens', ui.dim(String(result.usage.total_tokens))));
|
|
132
|
+
}
|
|
133
|
+
const dims = result.data[0]?.embedding?.length || 'N/A';
|
|
134
|
+
console.log(ui.label('Dimensions', ui.bold(String(dims))));
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const vector = result.data[0].embedding;
|
|
139
|
+
const preview = vector.slice(0, 5).map(v => v.toFixed(6)).join(', ');
|
|
140
|
+
console.log(`[${preview}, ...] (${vector.length} dims)`);
|
|
141
|
+
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(ui.success('Multimodal embedding generated'));
|
|
144
|
+
|
|
145
|
+
done({ dimensions: result.data[0]?.embedding?.length });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Standard text embedding path
|
|
31
150
|
const texts = await resolveTextInput(text, opts.file);
|
|
32
151
|
|
|
33
152
|
// --estimate: show cost comparison, optionally switch model
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('path');
|
|
4
|
-
|
|
4
|
+
let ora;
|
|
5
|
+
async function getOra() {
|
|
6
|
+
if (!ora) { ora = (await import('ora')).default; }
|
|
7
|
+
return ora;
|
|
8
|
+
}
|
|
5
9
|
const pc = require('picocolors');
|
|
6
10
|
|
|
7
11
|
/**
|
|
@@ -31,7 +35,7 @@ function registerIndexWorkspace(program) {
|
|
|
31
35
|
const { handleIndexWorkspace } = require('../mcp/tools/workspace');
|
|
32
36
|
const resolvedPath = workspacePath ? path.resolve(workspacePath) : process.cwd();
|
|
33
37
|
|
|
34
|
-
const spinner =
|
|
38
|
+
const spinner = (await getOra())(`Indexing ${resolvedPath}...`).start();
|
|
35
39
|
|
|
36
40
|
try {
|
|
37
41
|
const result = await handleIndexWorkspace({
|
|
@@ -74,7 +78,7 @@ function registerIndexWorkspace(program) {
|
|
|
74
78
|
|
|
75
79
|
// Create index if requested
|
|
76
80
|
if (opts.createIndex) {
|
|
77
|
-
const indexSpinner =
|
|
81
|
+
const indexSpinner = (await getOra())('Creating vector search index...').start();
|
|
78
82
|
try {
|
|
79
83
|
const { createVectorIndex } = require('../lib/mongo');
|
|
80
84
|
await createVectorIndex(
|
|
@@ -112,7 +116,7 @@ function registerIndexWorkspace(program) {
|
|
|
112
116
|
telemetry.send('cli_search_code_run', { language: opts.language });
|
|
113
117
|
|
|
114
118
|
const { handleSearchCode } = require('../mcp/tools/workspace');
|
|
115
|
-
const spinner =
|
|
119
|
+
const spinner = (await getOra())('Searching...').start();
|
|
116
120
|
|
|
117
121
|
try {
|
|
118
122
|
const result = await handleSearchCode({
|
|
@@ -198,7 +202,7 @@ function registerIndexWorkspace(program) {
|
|
|
198
202
|
process.exit(1);
|
|
199
203
|
}
|
|
200
204
|
|
|
201
|
-
const spinner =
|
|
205
|
+
const spinner = (await getOra())('Finding relevant context...').start();
|
|
202
206
|
|
|
203
207
|
try {
|
|
204
208
|
const result = await handleExplainCode({
|
|
@@ -134,7 +134,7 @@ function registerPlayground(program) {
|
|
|
134
134
|
*/
|
|
135
135
|
function createPlaygroundServer() {
|
|
136
136
|
const { getApiBase, requireApiKey, generateEmbeddings } = require('../lib/api');
|
|
137
|
-
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
137
|
+
const { MODEL_CATALOG, BENCHMARK_SCORES } = require('../lib/catalog');
|
|
138
138
|
const { cosineSimilarity } = require('../lib/math');
|
|
139
139
|
const { getConfigValue } = require('../lib/config');
|
|
140
140
|
|
|
@@ -273,6 +273,13 @@ function createPlaygroundServer() {
|
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
// API: Full Model Catalog (for Models tab)
|
|
277
|
+
if (req.method === 'GET' && req.url === '/api/models/catalog') {
|
|
278
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
279
|
+
res.end(JSON.stringify({ models: MODEL_CATALOG, benchmarks: BENCHMARK_SCORES }));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
276
283
|
// API: Generate code
|
|
277
284
|
if (req.method === 'POST' && req.url === '/api/generate') {
|
|
278
285
|
let body = '';
|
|
@@ -1399,9 +1406,58 @@ function createPlaygroundServer() {
|
|
|
1399
1406
|
res.end(JSON.stringify({ error: 'inputs must be a non-empty array' }));
|
|
1400
1407
|
return;
|
|
1401
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
// Optimize video inputs: downsample to 1fps to fit within 32k token context
|
|
1411
|
+
const os = require('os');
|
|
1412
|
+
const path = require('path');
|
|
1413
|
+
const fs = require('fs');
|
|
1414
|
+
const { execFileSync } = require('child_process');
|
|
1415
|
+
const optimizedInputs = [];
|
|
1416
|
+
for (const input of inputs) {
|
|
1417
|
+
const content = input.content;
|
|
1418
|
+
if (content && Array.isArray(content)) {
|
|
1419
|
+
const optimizedContent = [];
|
|
1420
|
+
for (const item of content) {
|
|
1421
|
+
if (item.type === 'video_base64' && item.video_base64) {
|
|
1422
|
+
// Downsample video to 1fps using ffmpeg to reduce token count
|
|
1423
|
+
try {
|
|
1424
|
+
const b64 = item.video_base64.replace(/^data:[^;]+;base64,/, '');
|
|
1425
|
+
const tmpIn = path.join(os.tmpdir(), `vai_vid_in_${Date.now()}.mp4`);
|
|
1426
|
+
const tmpOut = path.join(os.tmpdir(), `vai_vid_out_${Date.now()}.mp4`);
|
|
1427
|
+
fs.writeFileSync(tmpIn, Buffer.from(b64, 'base64'));
|
|
1428
|
+
try {
|
|
1429
|
+
execFileSync('ffmpeg', [
|
|
1430
|
+
'-y', '-i', tmpIn,
|
|
1431
|
+
'-vf', 'fps=1',
|
|
1432
|
+
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
|
|
1433
|
+
'-an', // strip audio
|
|
1434
|
+
tmpOut
|
|
1435
|
+
], { timeout: 30000, stdio: 'pipe' });
|
|
1436
|
+
const optimizedBuf = fs.readFileSync(tmpOut);
|
|
1437
|
+
const optimizedB64 = `data:video/mp4;base64,${optimizedBuf.toString('base64')}`;
|
|
1438
|
+
optimizedContent.push({ type: 'video_base64', video_base64: optimizedB64 });
|
|
1439
|
+
} finally {
|
|
1440
|
+
try { fs.unlinkSync(tmpIn); } catch (_) {}
|
|
1441
|
+
try { fs.unlinkSync(tmpOut); } catch (_) {}
|
|
1442
|
+
}
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
// If optimization fails, send original and let API error naturally
|
|
1445
|
+
console.warn('[Playground] Video optimization failed:', err.message);
|
|
1446
|
+
optimizedContent.push(item);
|
|
1447
|
+
}
|
|
1448
|
+
} else {
|
|
1449
|
+
optimizedContent.push(item);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
optimizedInputs.push({ ...input, content: optimizedContent });
|
|
1453
|
+
} else {
|
|
1454
|
+
optimizedInputs.push(input);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1402
1458
|
const { apiRequest } = require('../lib/api');
|
|
1403
1459
|
const mmBody = {
|
|
1404
|
-
inputs,
|
|
1460
|
+
inputs: optimizedInputs,
|
|
1405
1461
|
model: model || 'voyage-multimodal-3.5',
|
|
1406
1462
|
};
|
|
1407
1463
|
if (input_type) mmBody.input_type = input_type;
|
|
@@ -1583,9 +1639,13 @@ function createPlaygroundServer() {
|
|
|
1583
1639
|
else if (output.text) summary = output.text.slice(0, 100) + (output.text.length > 100 ? '...' : '');
|
|
1584
1640
|
else summary = JSON.stringify(output).slice(0, 200);
|
|
1585
1641
|
}
|
|
1642
|
+
// Extract usage data for cost tracking (then strip from output payload)
|
|
1643
|
+
const _usage = (output && output._usage) ? output._usage : undefined;
|
|
1644
|
+
const cleanOutput = _usage ? { ...output } : output;
|
|
1645
|
+
if (cleanOutput && cleanOutput._usage) delete cleanOutput._usage;
|
|
1586
1646
|
res.write(`event: step_complete\ndata: ${JSON.stringify({
|
|
1587
|
-
stepId, timeMs, summary,
|
|
1588
|
-
output: JSON.stringify(
|
|
1647
|
+
stepId, timeMs, summary, _usage,
|
|
1648
|
+
output: JSON.stringify(cleanOutput).length < 5000 ? cleanOutput : { _truncated: true, summary },
|
|
1589
1649
|
})}\n\n`);
|
|
1590
1650
|
},
|
|
1591
1651
|
onStepSkip: (stepId, reason) => {
|
|
@@ -1604,6 +1664,7 @@ function createPlaygroundServer() {
|
|
|
1604
1664
|
totalTimeMs: result.totalTimeMs,
|
|
1605
1665
|
layers: result.layers,
|
|
1606
1666
|
steps: result.steps,
|
|
1667
|
+
formatters: result.formatters || null,
|
|
1607
1668
|
})}\n\n`);
|
|
1608
1669
|
} catch (err) {
|
|
1609
1670
|
res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
@@ -83,7 +83,7 @@ We'll embed these ${SAMPLE_DOCS.length} sample documents:
|
|
|
83
83
|
console.log(pc.bold('\nStep 2: Embedding Documents'));
|
|
84
84
|
console.log(pc.dim('─'.repeat(40)));
|
|
85
85
|
console.log(`
|
|
86
|
-
Running: ${pc.cyan('vai embed --model voyage-
|
|
86
|
+
Running: ${pc.cyan('vai embed --model voyage-4-lite')}
|
|
87
87
|
`);
|
|
88
88
|
|
|
89
89
|
let embeddings;
|
|
@@ -91,7 +91,7 @@ Running: ${pc.cyan('vai embed --model voyage-3-lite')}
|
|
|
91
91
|
process.stdout.write(' Embedding documents... ');
|
|
92
92
|
const result = await embed({
|
|
93
93
|
texts: SAMPLE_DOCS,
|
|
94
|
-
model: 'voyage-
|
|
94
|
+
model: 'voyage-4-lite',
|
|
95
95
|
inputType: 'document',
|
|
96
96
|
});
|
|
97
97
|
embeddings = result.embeddings;
|
|
@@ -99,7 +99,7 @@ Running: ${pc.cyan('vai embed --model voyage-3-lite')}
|
|
|
99
99
|
console.log(`
|
|
100
100
|
${pc.green('✓')} Created ${embeddings.length} embeddings
|
|
101
101
|
${pc.dim(` Dimensions: ${embeddings[0].length}`)}
|
|
102
|
-
${pc.dim(` Model: voyage-
|
|
102
|
+
${pc.dim(` Model: voyage-4-lite`)}
|
|
103
103
|
`);
|
|
104
104
|
} catch (err) {
|
|
105
105
|
console.log(pc.red('✗'));
|
|
@@ -123,7 +123,7 @@ Query: "${pc.cyan(query)}"
|
|
|
123
123
|
process.stdout.write(' Embedding query... ');
|
|
124
124
|
const queryResult = await embed({
|
|
125
125
|
texts: [query],
|
|
126
|
-
model: 'voyage-
|
|
126
|
+
model: 'voyage-4-lite',
|
|
127
127
|
inputType: 'query',
|
|
128
128
|
});
|
|
129
129
|
const queryEmbedding = queryResult.embeddings[0];
|
package/src/commands/workflow.js
CHANGED
|
@@ -48,8 +48,13 @@ async function promptForInputs(definition, existingInputs) {
|
|
|
48
48
|
const { buildInputSteps } = require('../lib/workflow');
|
|
49
49
|
const { createCLIRenderer } = require('../lib/wizard-cli');
|
|
50
50
|
const { runWizard } = require('../lib/wizard');
|
|
51
|
+
const { loadInputCache } = require('../lib/workflow-input-cache');
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
// Load cached inputs from last run to use as defaults
|
|
54
|
+
const workflowName = definition.name || '';
|
|
55
|
+
const cachedInputs = loadInputCache(workflowName);
|
|
56
|
+
|
|
57
|
+
const allSteps = buildInputSteps(definition, cachedInputs);
|
|
53
58
|
// Only prompt for inputs not already provided
|
|
54
59
|
const steps = allSteps.filter(s => !(s.id in existingInputs));
|
|
55
60
|
if (steps.length === 0) return existingInputs;
|
|
@@ -93,6 +98,7 @@ function registerWorkflow(program) {
|
|
|
93
98
|
.option('--db <name>', 'Override default database')
|
|
94
99
|
.option('--collection <name>', 'Override default collection')
|
|
95
100
|
.option('--json', 'Output results as JSON', false)
|
|
101
|
+
.option('-o, --output <format>', 'Output format: json, table, markdown, text, csv, value:<path>')
|
|
96
102
|
.option('--quiet', 'Suppress progress output', false)
|
|
97
103
|
.option('--dry-run', 'Show execution plan without running', false)
|
|
98
104
|
.option('--verbose', 'Show step details', false)
|
|
@@ -232,33 +238,24 @@ function registerWorkflow(program) {
|
|
|
232
238
|
|
|
233
239
|
wfDone();
|
|
234
240
|
|
|
241
|
+
// Cache inputs for next run
|
|
242
|
+
try {
|
|
243
|
+
const { saveInputCache } = require('../lib/workflow-input-cache');
|
|
244
|
+
saveInputCache(workflowName, opts.input);
|
|
245
|
+
} catch { /* non-critical, don't fail the run */ }
|
|
246
|
+
|
|
235
247
|
// Output
|
|
236
|
-
|
|
248
|
+
const { formatWorkflowOutput, autoDetectFormat } = require('../lib/workflow-formatters');
|
|
249
|
+
const fmtHints = definition.formatters || {};
|
|
250
|
+
|
|
251
|
+
if (opts.json || opts.output === 'json') {
|
|
237
252
|
console.log(JSON.stringify(result.output, null, 2));
|
|
253
|
+
} else if (opts.output) {
|
|
254
|
+
console.log(formatWorkflowOutput(result.output, opts.output, fmtHints));
|
|
238
255
|
} else if (result.output) {
|
|
239
|
-
//
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const top = output.results.slice(0, 5);
|
|
243
|
-
console.log(pc.bold('Top results:'));
|
|
244
|
-
for (let i = 0; i < top.length; i++) {
|
|
245
|
-
const r = top[i];
|
|
246
|
-
const source = r.source || r.text?.slice(0, 50) || `result ${i + 1}`;
|
|
247
|
-
const score = r.score != null ? ` (${r.score.toFixed(2)})` : '';
|
|
248
|
-
console.log(` ${pc.dim(`[${i + 1}]`)} ${source}${pc.dim(score)}`);
|
|
249
|
-
}
|
|
250
|
-
} else if (output.summary) {
|
|
251
|
-
console.log(output.summary);
|
|
252
|
-
} else if (output.comparison) {
|
|
253
|
-
console.log(pc.bold('Cost comparison:'));
|
|
254
|
-
for (const item of output.comparison) {
|
|
255
|
-
if (item && item.model) {
|
|
256
|
-
console.log(` ${pc.cyan(item.model)}: $${item.totalCost} total (embed: $${item.embeddingCost}, queries: $${item.monthlyQueryCost}/mo)`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
} else {
|
|
260
|
-
console.log(JSON.stringify(output, null, 2));
|
|
261
|
-
}
|
|
256
|
+
// Auto-detect best format from output shape and workflow hints
|
|
257
|
+
const bestFormat = autoDetectFormat(result.output, fmtHints);
|
|
258
|
+
console.log(formatWorkflowOutput(result.output, bestFormat, fmtHints));
|
|
262
259
|
}
|
|
263
260
|
} catch (err) {
|
|
264
261
|
console.error(ui.error(err.message));
|
|
@@ -862,8 +859,9 @@ function registerWorkflow(program) {
|
|
|
862
859
|
// ── workflow create ──
|
|
863
860
|
wfCmd
|
|
864
861
|
.command('create')
|
|
865
|
-
.description('
|
|
862
|
+
.description('Interactive workflow builder -- scaffold a validated, publish-ready workflow package')
|
|
866
863
|
.option('--from <file>', 'Existing workflow JSON to package')
|
|
864
|
+
.option('--from-description <desc>', 'Generate a workflow skeleton from a text description')
|
|
867
865
|
.option('--name <name>', 'Package name (without vai-workflow- prefix)')
|
|
868
866
|
.option('--author <name>', 'Author name')
|
|
869
867
|
.option('--description <desc>', 'Package description')
|
|
@@ -871,8 +869,9 @@ function registerWorkflow(program) {
|
|
|
871
869
|
.option('--scope <scope>', 'Package scope (e.g. "vaicli" for @vaicli/vai-workflow-*)')
|
|
872
870
|
.option('--output <dir>', 'Output directory')
|
|
873
871
|
.action(async (opts) => {
|
|
874
|
-
const { scaffoldPackage, toPackageName, CATEGORIES
|
|
875
|
-
const { loadWorkflow } = require('../lib/workflow');
|
|
872
|
+
const { scaffoldPackage, toPackageName, CATEGORIES } = require('../lib/workflow-scaffold');
|
|
873
|
+
const { loadWorkflow, validateWorkflow, buildExecutionPlan } = require('../lib/workflow');
|
|
874
|
+
const { runInteractiveBuilder, workflowFromDescription } = require('../lib/workflow-builder');
|
|
876
875
|
|
|
877
876
|
let definition;
|
|
878
877
|
let name = opts.name;
|
|
@@ -894,50 +893,85 @@ function registerWorkflow(program) {
|
|
|
894
893
|
if (!description) {
|
|
895
894
|
description = definition.description;
|
|
896
895
|
}
|
|
897
|
-
} else if (
|
|
898
|
-
//
|
|
896
|
+
} else if (opts.fromDescription) {
|
|
897
|
+
// Generate from text description
|
|
899
898
|
try {
|
|
899
|
+
definition = workflowFromDescription(opts.fromDescription);
|
|
900
|
+
name = name || definition.name;
|
|
901
|
+
description = description || definition.description;
|
|
902
|
+
|
|
900
903
|
const p = require('@clack/prompts');
|
|
901
|
-
p.intro(pc.bold('
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
904
|
+
p.intro(pc.bold('Generated workflow from description'));
|
|
905
|
+
|
|
906
|
+
// Show what was generated
|
|
907
|
+
p.log.info(`Name: ${pc.cyan(definition.name)}`);
|
|
908
|
+
p.log.info(`Steps: ${definition.steps.map(s => `${pc.cyan(s.id)} (${s.tool})`).join(' -> ')}`);
|
|
909
|
+
p.log.info(`Inputs: ${Object.keys(definition.inputs).join(', ') || 'none'}`);
|
|
910
|
+
|
|
911
|
+
// Show execution plan
|
|
912
|
+
try {
|
|
913
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
914
|
+
p.log.info(pc.bold('Execution plan:'));
|
|
915
|
+
for (let i = 0; i < layers.length; i++) {
|
|
916
|
+
p.log.message(` Layer ${i + 1}: ${layers[i].join(', ')}`);
|
|
917
|
+
}
|
|
918
|
+
} catch (e) { /* skip */ }
|
|
919
|
+
|
|
920
|
+
// Validate
|
|
921
|
+
const errors = validateWorkflow(definition);
|
|
922
|
+
if (errors.length > 0) {
|
|
923
|
+
p.log.warn('Validation issues:');
|
|
924
|
+
for (const err of errors) {
|
|
925
|
+
p.log.warn(` ${err}`);
|
|
926
|
+
}
|
|
927
|
+
} else {
|
|
928
|
+
p.log.success('Workflow validates successfully!');
|
|
916
929
|
}
|
|
917
930
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
931
|
+
// Ask for category if not provided
|
|
932
|
+
if (!category) {
|
|
933
|
+
const { guessCategory, extractTools } = require('../lib/workflow-scaffold');
|
|
934
|
+
category = guessCategory(extractTools(definition));
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Ask for author if not provided
|
|
938
|
+
if (!author) {
|
|
939
|
+
author = getGitAuthor();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// Confirm or edit
|
|
943
|
+
const proceed = await p.confirm({ message: 'Scaffold this workflow as a package?', initialValue: true });
|
|
944
|
+
if (p.isCancel(proceed) || !proceed) {
|
|
945
|
+
// Write just the workflow.json for manual editing
|
|
946
|
+
const fs = require('fs');
|
|
947
|
+
const filename = `${definition.name}.vai-workflow.json`;
|
|
948
|
+
fs.writeFileSync(filename, JSON.stringify(definition, null, 2) + '\n');
|
|
949
|
+
p.log.info(`Wrote ${pc.cyan(filename)} for manual editing.`);
|
|
950
|
+
p.outro('Edit the file and run `vai workflow create --from <file>` when ready.');
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
935
953
|
} catch (err) {
|
|
954
|
+
console.error(ui.error(`Generation failed: ${err.message}`));
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
} else if (process.stdin.isTTY) {
|
|
958
|
+
// Full interactive builder
|
|
959
|
+
try {
|
|
960
|
+
const result = await runInteractiveBuilder();
|
|
961
|
+
definition = result.definition;
|
|
962
|
+
name = name || result.name;
|
|
963
|
+
description = description || result.description;
|
|
964
|
+
category = category || result.category;
|
|
965
|
+
author = author || result.author;
|
|
966
|
+
} catch (err) {
|
|
967
|
+
if (err.message && err.message.includes('cancelled')) {
|
|
968
|
+
process.exit(0);
|
|
969
|
+
}
|
|
936
970
|
console.error(ui.error(`Interactive mode failed: ${err.message}`));
|
|
937
971
|
process.exit(1);
|
|
938
972
|
}
|
|
939
973
|
} else {
|
|
940
|
-
console.error(ui.error('Provide --from <file
|
|
974
|
+
console.error(ui.error('Provide --from <file>, --from-description "text", or run interactively (TTY required).'));
|
|
941
975
|
process.exit(1);
|
|
942
976
|
}
|
|
943
977
|
|
|
@@ -965,8 +999,8 @@ function registerWorkflow(program) {
|
|
|
965
999
|
}
|
|
966
1000
|
console.log();
|
|
967
1001
|
console.log('Next steps:');
|
|
968
|
-
console.log(` 1. ${opts.from ? '
|
|
969
|
-
console.log(` 2.
|
|
1002
|
+
console.log(` 1. ${opts.from ? 'Review README.md' : `cd ${pkgName} && review workflow.json`}`);
|
|
1003
|
+
console.log(` 2. ${pc.dim('vai workflow validate')} ${pkgName}/workflow.json`);
|
|
970
1004
|
console.log(` 3. npm publish`);
|
|
971
1005
|
console.log();
|
|
972
1006
|
} catch (err) {
|
|
@@ -1021,6 +1055,39 @@ function registerWorkflow(program) {
|
|
|
1021
1055
|
console.log(` ${pc.dim('Run with:')} vai workflow run ${filename} --input query="your question"`);
|
|
1022
1056
|
console.log(` ${pc.dim('Validate:')} vai workflow validate ${filename}`);
|
|
1023
1057
|
});
|
|
1058
|
+
|
|
1059
|
+
// ── workflow clear-cache [name] ──
|
|
1060
|
+
wfCmd
|
|
1061
|
+
.command('clear-cache [name]')
|
|
1062
|
+
.description('Clear cached workflow inputs (from previous runs)')
|
|
1063
|
+
.action((name) => {
|
|
1064
|
+
const { clearInputCache, loadInputCache, slugify, CACHE_PATH } = require('../lib/workflow-input-cache');
|
|
1065
|
+
const fs = require('fs');
|
|
1066
|
+
|
|
1067
|
+
if (name) {
|
|
1068
|
+
const cached = loadInputCache(name);
|
|
1069
|
+
const keys = Object.keys(cached);
|
|
1070
|
+
if (keys.length === 0) {
|
|
1071
|
+
console.log(pc.dim(`No cached inputs for "${name}".`));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
clearInputCache(name);
|
|
1075
|
+
console.log(ui.success(`Cleared cached inputs for "${name}" (${keys.length} field${keys.length === 1 ? '' : 's'}).`));
|
|
1076
|
+
} else {
|
|
1077
|
+
// Clear all
|
|
1078
|
+
let count = 0;
|
|
1079
|
+
try {
|
|
1080
|
+
const raw = fs.readFileSync(CACHE_PATH, 'utf-8');
|
|
1081
|
+
count = Object.keys(JSON.parse(raw)).length;
|
|
1082
|
+
} catch { /* no file */ }
|
|
1083
|
+
if (count === 0) {
|
|
1084
|
+
console.log(pc.dim('No cached workflow inputs.'));
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
clearInputCache();
|
|
1088
|
+
console.log(ui.success(`Cleared cached inputs for ${count} workflow${count === 1 ? '' : 's'}.`));
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1024
1091
|
}
|
|
1025
1092
|
|
|
1026
1093
|
/**
|
package/src/lib/api.js
CHANGED
|
@@ -195,6 +195,36 @@ async function generateEmbeddings(texts, options = {}) {
|
|
|
195
195
|
return apiRequest('/embeddings', body);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Generate multimodal embeddings for inputs containing text, images, and/or video.
|
|
200
|
+
* Uses the /multimodalembeddings endpoint with a different input format.
|
|
201
|
+
* @param {Array<Array<{type: string, text?: string, image_base64?: string, video_base64?: string}>>} inputs
|
|
202
|
+
* Array of content arrays. Each content array is a list of content items for one input.
|
|
203
|
+
* Example: [[{type: 'text', text: 'hello'}, {type: 'image_base64', image_base64: 'data:image/png;base64,...'}]]
|
|
204
|
+
* @param {object} options
|
|
205
|
+
* @param {string} [options.model] - Model name (default: voyage-multimodal-3.5)
|
|
206
|
+
* @param {string} [options.inputType] - Input type (query|document)
|
|
207
|
+
* @param {number} [options.outputDimension] - Output dimensions
|
|
208
|
+
* @returns {Promise<object>} API response with embeddings
|
|
209
|
+
*/
|
|
210
|
+
async function generateMultimodalEmbeddings(inputs, options = {}) {
|
|
211
|
+
const model = options.model || 'voyage-multimodal-3.5';
|
|
212
|
+
|
|
213
|
+
const body = {
|
|
214
|
+
inputs: inputs.map(contentArray => ({ content: contentArray })),
|
|
215
|
+
model,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
if (options.inputType) {
|
|
219
|
+
body.input_type = options.inputType;
|
|
220
|
+
}
|
|
221
|
+
if (options.outputDimension) {
|
|
222
|
+
body.output_dimension = options.outputDimension;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return apiRequest('/multimodalembeddings', body);
|
|
226
|
+
}
|
|
227
|
+
|
|
198
228
|
module.exports = {
|
|
199
229
|
API_BASE,
|
|
200
230
|
ATLAS_API_BASE,
|
|
@@ -204,4 +234,5 @@ module.exports = {
|
|
|
204
234
|
requireApiKey,
|
|
205
235
|
apiRequest,
|
|
206
236
|
generateEmbeddings,
|
|
237
|
+
generateMultimodalEmbeddings,
|
|
207
238
|
};
|