voyageai-cli 1.27.0 → 1.29.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/package.json +2 -1
- package/src/commands/app.js +15 -0
- package/src/commands/config.js +33 -0
- package/src/commands/mcp-server.js +4 -1
- package/src/commands/playground.js +657 -9
- package/src/commands/workflow.js +461 -13
- package/src/lib/api.js +40 -2
- package/src/lib/explanations.js +88 -0
- package/src/lib/npm-utils.js +265 -0
- package/src/lib/workflow-registry.js +416 -0
- package/src/lib/workflow-scaffold.js +319 -0
- package/src/lib/workflow.js +530 -8
- package/src/mcp/server.js +15 -2
- package/src/mcp/sse-transport.js +112 -0
- package/src/playground/announcements.md +71 -0
- package/src/playground/icons/V.png +0 -0
- package/src/playground/index.html +3536 -461
- package/src/workflows/consistency-check.json +4 -0
- package/src/workflows/cost-analysis.json +4 -0
- package/src/workflows/enrich-and-ingest.json +56 -0
- package/src/workflows/intelligent-ingest.json +66 -0
- package/src/workflows/kb-health-report.json +45 -0
- package/src/workflows/multi-collection-search.json +4 -0
- package/src/workflows/research-and-summarize.json +4 -0
- package/src/workflows/search-with-fallback.json +66 -0
- package/src/workflows/smart-ingest.json +4 -0
package/src/lib/explanations.js
CHANGED
|
@@ -1478,6 +1478,87 @@ const concepts = {
|
|
|
1478
1478
|
'vai workflow run multi-collection-search --input query="test" --dry-run',
|
|
1479
1479
|
],
|
|
1480
1480
|
},
|
|
1481
|
+
|
|
1482
|
+
'workflow-publishing': {
|
|
1483
|
+
title: 'Publishing vai Workflows to npm',
|
|
1484
|
+
summary: 'Share workflows as npm packages using the vai-workflow-* convention',
|
|
1485
|
+
content: [
|
|
1486
|
+
`${pc.bold('WHAT IS A WORKFLOW PACKAGE?')}`,
|
|
1487
|
+
``,
|
|
1488
|
+
`A vai workflow package is an npm package containing a reusable workflow`,
|
|
1489
|
+
`definition. It follows the naming convention ${pc.cyan('vai-workflow-<name>')} and contains:`,
|
|
1490
|
+
``,
|
|
1491
|
+
` • ${pc.cyan('workflow.json')} — The workflow definition (same format as any vai workflow)`,
|
|
1492
|
+
` • ${pc.cyan('package.json')} — npm metadata + vai-specific fields`,
|
|
1493
|
+
` • ${pc.cyan('README.md')} — Usage instructions`,
|
|
1494
|
+
``,
|
|
1495
|
+
`No JavaScript, no build step, no dependencies.`,
|
|
1496
|
+
``,
|
|
1497
|
+
`${pc.bold('HOW TO PUBLISH')}`,
|
|
1498
|
+
``,
|
|
1499
|
+
`Option 1: Package an existing workflow`,
|
|
1500
|
+
``,
|
|
1501
|
+
` ${pc.cyan('vai workflow create --from my-workflow.json --name my-workflow')}`,
|
|
1502
|
+
``,
|
|
1503
|
+
` This creates a ${pc.cyan('vai-workflow-my-workflow/')} directory with everything needed.`,
|
|
1504
|
+
` Review it, then:`,
|
|
1505
|
+
``,
|
|
1506
|
+
` ${pc.cyan('cd vai-workflow-my-workflow')}`,
|
|
1507
|
+
` ${pc.cyan('npm publish')}`,
|
|
1508
|
+
``,
|
|
1509
|
+
`Option 2: Start from scratch`,
|
|
1510
|
+
``,
|
|
1511
|
+
` ${pc.cyan('vai workflow create --name my-workflow')}`,
|
|
1512
|
+
``,
|
|
1513
|
+
` This creates an interactive template you can fill in.`,
|
|
1514
|
+
``,
|
|
1515
|
+
`${pc.bold('PACKAGE.JSON REQUIREMENTS')}`,
|
|
1516
|
+
``,
|
|
1517
|
+
`Your package.json needs a ${pc.cyan('"vai"')} field with:`,
|
|
1518
|
+
``,
|
|
1519
|
+
` {`,
|
|
1520
|
+
` "vai": {`,
|
|
1521
|
+
` "workflowVersion": "1.0",`,
|
|
1522
|
+
` "tools": ["query", "rerank"],`,
|
|
1523
|
+
` "category": "retrieval"`,
|
|
1524
|
+
` }`,
|
|
1525
|
+
` }`,
|
|
1526
|
+
``,
|
|
1527
|
+
`${pc.cyan('vai workflow create')} fills this in automatically.`,
|
|
1528
|
+
``,
|
|
1529
|
+
`${pc.bold('NAMING')}`,
|
|
1530
|
+
``,
|
|
1531
|
+
`Package names must start with ${pc.cyan('vai-workflow-')} followed by a lowercase, hyphenated name:`,
|
|
1532
|
+
``,
|
|
1533
|
+
` ${pc.green('✓')} vai-workflow-legal-research`,
|
|
1534
|
+
` ${pc.green('✓')} vai-workflow-code-review`,
|
|
1535
|
+
` ${pc.red('✗')} vai-legal-workflow (wrong prefix)`,
|
|
1536
|
+
``,
|
|
1537
|
+
`${pc.bold('CATEGORIES')}`,
|
|
1538
|
+
``,
|
|
1539
|
+
` ${pc.cyan('retrieval')} — Search and retrieval pipelines`,
|
|
1540
|
+
` ${pc.cyan('analysis')} — Comparison, consistency checking`,
|
|
1541
|
+
` ${pc.cyan('ingestion')} — Document processing and storage`,
|
|
1542
|
+
` ${pc.cyan('domain-specific')} — Industry-focused (legal, clinical, etc.)`,
|
|
1543
|
+
` ${pc.cyan('utility')} — Cost estimation, benchmarking`,
|
|
1544
|
+
` ${pc.cyan('integration')} — Multi-system workflows`,
|
|
1545
|
+
``,
|
|
1546
|
+
`${pc.bold('TESTING BEFORE PUBLISHING')}`,
|
|
1547
|
+
``,
|
|
1548
|
+
` ${pc.cyan('vai workflow validate ./vai-workflow-my-workflow/workflow.json')}`,
|
|
1549
|
+
` ${pc.cyan('npm install ./vai-workflow-my-workflow')}`,
|
|
1550
|
+
` ${pc.cyan('vai workflow run vai-workflow-my-workflow --dry-run')}`,
|
|
1551
|
+
].join('\n'),
|
|
1552
|
+
links: [
|
|
1553
|
+
'https://github.com/mrlynn/voyageai-cli',
|
|
1554
|
+
],
|
|
1555
|
+
tryIt: [
|
|
1556
|
+
'vai workflow create --from my-pipeline.json --name my-pipeline',
|
|
1557
|
+
'vai workflow create',
|
|
1558
|
+
'vai workflow search legal',
|
|
1559
|
+
'vai workflow install vai-workflow-legal-research',
|
|
1560
|
+
],
|
|
1561
|
+
},
|
|
1481
1562
|
};
|
|
1482
1563
|
|
|
1483
1564
|
/**
|
|
@@ -1651,6 +1732,13 @@ const aliases = {
|
|
|
1651
1732
|
'vai-workflow': 'workflows',
|
|
1652
1733
|
pipeline: 'workflows',
|
|
1653
1734
|
dag: 'workflows',
|
|
1735
|
+
// Workflow publishing aliases
|
|
1736
|
+
'workflow-publishing': 'workflow-publishing',
|
|
1737
|
+
'publish-workflow': 'workflow-publishing',
|
|
1738
|
+
'workflow-registry': 'workflow-publishing',
|
|
1739
|
+
'community-workflows': 'workflow-publishing',
|
|
1740
|
+
'share-workflow': 'workflow-publishing',
|
|
1741
|
+
'npm-workflow': 'workflow-publishing',
|
|
1654
1742
|
};
|
|
1655
1743
|
|
|
1656
1744
|
/**
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
const WORKFLOW_PREFIX = 'vai-workflow-';
|
|
8
|
+
const VAICLI_SCOPE = '@vaicli/';
|
|
9
|
+
const VAICLI_WORKFLOW_PREFIX = '@vaicli/vai-workflow-';
|
|
10
|
+
const NPM_SEARCH_URL = 'https://registry.npmjs.org/-/v1/search';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if a package name is an official @vaicli scoped package.
|
|
14
|
+
* @param {string} name
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isOfficialPackage(name) {
|
|
18
|
+
return name.startsWith(VAICLI_WORKFLOW_PREFIX);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a package name is a valid vai workflow package (scoped or unscoped).
|
|
23
|
+
* @param {string} name
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isWorkflowPackage(name) {
|
|
27
|
+
return name.startsWith(VAICLI_WORKFLOW_PREFIX) || name.startsWith(WORKFLOW_PREFIX);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Search the npm registry for vai workflow packages.
|
|
32
|
+
* @param {string} query - Search terms
|
|
33
|
+
* @param {{ limit?: number }} options
|
|
34
|
+
* @returns {Promise<Array<{ name: string, version: string, description: string, author: string, date: string, keywords: string[], official: boolean }>>}
|
|
35
|
+
*/
|
|
36
|
+
async function searchNpm(query, options = {}) {
|
|
37
|
+
const limit = options.limit || 10;
|
|
38
|
+
// Search for both scoped and unscoped packages
|
|
39
|
+
const searchText = query
|
|
40
|
+
? `keywords:vai-workflow ${query}`
|
|
41
|
+
: `keywords:vai-workflow`;
|
|
42
|
+
const url = `${NPM_SEARCH_URL}?text=${encodeURIComponent(searchText)}&size=${limit * 3}`;
|
|
43
|
+
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
headers: { 'Accept': 'application/json' },
|
|
46
|
+
signal: AbortSignal.timeout(15000),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(`npm search failed: ${res.status} ${res.statusText}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
let results = (data.objects || [])
|
|
55
|
+
.filter(obj => isWorkflowPackage(obj.package.name))
|
|
56
|
+
.map(obj => ({
|
|
57
|
+
name: obj.package.name,
|
|
58
|
+
version: obj.package.version,
|
|
59
|
+
description: obj.package.description || '',
|
|
60
|
+
author: obj.package.author?.name || obj.package.publisher?.username || 'unknown',
|
|
61
|
+
date: obj.package.date,
|
|
62
|
+
keywords: obj.package.keywords || [],
|
|
63
|
+
official: isOfficialPackage(obj.package.name),
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Client-side filtering: npm keyword search returns all vai-workflow packages,
|
|
67
|
+
// so we filter locally by matching query against name, description, and keywords
|
|
68
|
+
if (query) {
|
|
69
|
+
const q = query.toLowerCase();
|
|
70
|
+
const terms = q.split(/\s+/).filter(Boolean);
|
|
71
|
+
results = results.filter(pkg => {
|
|
72
|
+
const haystack = [
|
|
73
|
+
pkg.name,
|
|
74
|
+
pkg.description,
|
|
75
|
+
...pkg.keywords,
|
|
76
|
+
].join(' ').toLowerCase();
|
|
77
|
+
return terms.every(term => haystack.includes(term));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
results = results.slice(0, options.limit || limit);
|
|
82
|
+
|
|
83
|
+
// Sort: official first, then by name
|
|
84
|
+
results.sort((a, b) => {
|
|
85
|
+
if (a.official !== b.official) return a.official ? -1 : 1;
|
|
86
|
+
return a.name.localeCompare(b.name);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Install a workflow package via npm.
|
|
94
|
+
* @param {string} packageName
|
|
95
|
+
* @param {{ global?: boolean }} options
|
|
96
|
+
* @returns {{ success: boolean, version: string, path: string, official: boolean }}
|
|
97
|
+
*/
|
|
98
|
+
function installPackage(packageName, options = {}) {
|
|
99
|
+
if (!isWorkflowPackage(packageName)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Package name must start with "${WORKFLOW_PREFIX}" or "${VAICLI_WORKFLOW_PREFIX}". Did you mean ${WORKFLOW_PREFIX}${packageName}?`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Determine install location: use global if explicitly requested,
|
|
106
|
+
// or if there's no local package.json (e.g. running inside Electron app)
|
|
107
|
+
const hasLocalPkg = !options.global && (() => {
|
|
108
|
+
try { return require('fs').existsSync(require('path').join(process.cwd(), 'package.json')); }
|
|
109
|
+
catch { return false; }
|
|
110
|
+
})();
|
|
111
|
+
const useGlobal = options.global || !hasLocalPkg;
|
|
112
|
+
const globalFlag = useGlobal ? '-g' : '';
|
|
113
|
+
const cmd = `npm install ${packageName} ${globalFlag} ${useGlobal ? '' : '--save'} --ignore-scripts 2>&1`;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const output = execSync(cmd, {
|
|
117
|
+
encoding: 'utf8',
|
|
118
|
+
timeout: 60000,
|
|
119
|
+
cwd: useGlobal ? undefined : process.cwd(),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Find installed version from node_modules
|
|
123
|
+
const pkgPath = resolvePackagePath(packageName, useGlobal);
|
|
124
|
+
let version = 'unknown';
|
|
125
|
+
if (pkgPath) {
|
|
126
|
+
try {
|
|
127
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf8'));
|
|
128
|
+
version = pkg.version;
|
|
129
|
+
} catch { /* ignore */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { success: true, version, path: pkgPath || '', official: isOfficialPackage(packageName) };
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const msg = err.stderr || err.stdout || err.message;
|
|
135
|
+
if (msg.includes('404') || msg.includes('E404')) {
|
|
136
|
+
throw new Error(`Package ${packageName} not found on npm`);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(`npm install failed: ${msg}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Uninstall a workflow package via npm.
|
|
144
|
+
* @param {string} packageName
|
|
145
|
+
* @param {{ global?: boolean }} options
|
|
146
|
+
* @returns {{ success: boolean }}
|
|
147
|
+
*/
|
|
148
|
+
function uninstallPackage(packageName, options = {}) {
|
|
149
|
+
const globalFlag = options.global ? '-g' : '';
|
|
150
|
+
const cmd = `npm uninstall ${packageName} ${globalFlag} 2>&1`;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
execSync(cmd, { encoding: 'utf8', timeout: 30000 });
|
|
154
|
+
return { success: true };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
throw new Error(`npm uninstall failed: ${err.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get metadata for a package from the npm registry (without installing).
|
|
162
|
+
* @param {string} packageName
|
|
163
|
+
* @returns {Promise<{ name: string, version: string, description: string, author: string, vai: object|null, official: boolean }>}
|
|
164
|
+
*/
|
|
165
|
+
async function getPackageInfo(packageName) {
|
|
166
|
+
// Scoped packages need proper encoding: @vaicli/vai-workflow-foo -> @vaicli%2fvai-workflow-foo
|
|
167
|
+
const encodedName = packageName.startsWith('@')
|
|
168
|
+
? `@${encodeURIComponent(packageName.slice(1))}`
|
|
169
|
+
: encodeURIComponent(packageName);
|
|
170
|
+
const url = `https://registry.npmjs.org/${encodedName}/latest`;
|
|
171
|
+
const res = await fetch(url, {
|
|
172
|
+
headers: { 'Accept': 'application/json' },
|
|
173
|
+
signal: AbortSignal.timeout(10000),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
if (res.status === 404) throw new Error(`Package ${packageName} not found on npm`);
|
|
178
|
+
throw new Error(`npm registry error: ${res.status}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const data = await res.json();
|
|
182
|
+
return {
|
|
183
|
+
name: data.name,
|
|
184
|
+
version: data.version,
|
|
185
|
+
description: data.description || '',
|
|
186
|
+
author: typeof data.author === 'string' ? data.author : data.author?.name || 'unknown',
|
|
187
|
+
license: data.license || 'unknown',
|
|
188
|
+
vai: data.vai || null,
|
|
189
|
+
keywords: data.keywords || [],
|
|
190
|
+
repository: data.repository,
|
|
191
|
+
official: isOfficialPackage(data.name),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Resolve the filesystem path of an installed package.
|
|
197
|
+
* Handles both scoped (@vaicli/vai-workflow-*) and unscoped (vai-workflow-*) packages.
|
|
198
|
+
* @param {string} packageName
|
|
199
|
+
* @param {boolean} [global]
|
|
200
|
+
* @returns {string|null}
|
|
201
|
+
*/
|
|
202
|
+
function resolvePackagePath(packageName, global) {
|
|
203
|
+
// Scoped packages live at node_modules/@scope/package-name
|
|
204
|
+
// path.join handles this correctly since packageName includes the scope
|
|
205
|
+
if (!global) {
|
|
206
|
+
let dir = process.cwd();
|
|
207
|
+
while (dir !== path.dirname(dir)) {
|
|
208
|
+
const candidate = path.join(dir, 'node_modules', packageName);
|
|
209
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
210
|
+
dir = path.dirname(dir);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Try global
|
|
215
|
+
try {
|
|
216
|
+
const globalRoot = execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
217
|
+
const candidate = path.join(globalRoot, packageName);
|
|
218
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
219
|
+
} catch { /* ignore */ }
|
|
220
|
+
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find the nearest node_modules directory.
|
|
226
|
+
* @returns {string|null}
|
|
227
|
+
*/
|
|
228
|
+
function findLocalNodeModules() {
|
|
229
|
+
let dir = process.cwd();
|
|
230
|
+
while (dir !== path.dirname(dir)) {
|
|
231
|
+
const candidate = path.join(dir, 'node_modules');
|
|
232
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
233
|
+
return candidate;
|
|
234
|
+
}
|
|
235
|
+
dir = path.dirname(dir);
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Find the global node_modules directory.
|
|
242
|
+
* @returns {string|null}
|
|
243
|
+
*/
|
|
244
|
+
function findGlobalNodeModules() {
|
|
245
|
+
try {
|
|
246
|
+
return execSync('npm root -g', { encoding: 'utf8' }).trim();
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
searchNpm,
|
|
254
|
+
installPackage,
|
|
255
|
+
uninstallPackage,
|
|
256
|
+
getPackageInfo,
|
|
257
|
+
resolvePackagePath,
|
|
258
|
+
findLocalNodeModules,
|
|
259
|
+
findGlobalNodeModules,
|
|
260
|
+
isOfficialPackage,
|
|
261
|
+
isWorkflowPackage,
|
|
262
|
+
WORKFLOW_PREFIX,
|
|
263
|
+
VAICLI_SCOPE,
|
|
264
|
+
VAICLI_WORKFLOW_PREFIX,
|
|
265
|
+
};
|