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/commands/workflow.js
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const path = require('path');
|
|
3
4
|
const pc = require('picocolors');
|
|
4
5
|
const ui = require('../lib/ui');
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Try to get the git user name for default author.
|
|
9
|
+
* @returns {string}
|
|
10
|
+
*/
|
|
11
|
+
function getGitAuthor() {
|
|
12
|
+
try {
|
|
13
|
+
const { execSync } = require('child_process');
|
|
14
|
+
return execSync('git config user.name', { encoding: 'utf8' }).trim();
|
|
15
|
+
} catch {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* Parse repeatable --input key=value options into an object.
|
|
8
22
|
* Used as Commander's option reducer.
|
|
@@ -22,6 +36,45 @@ function collectInputs(pair, prev) {
|
|
|
22
36
|
return prev;
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Interactively prompt the user for missing workflow inputs using @clack/prompts.
|
|
41
|
+
* Only prompts for inputs not already provided via --input flags.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} definition - Workflow definition
|
|
44
|
+
* @param {object} existingInputs - Inputs already provided via --input
|
|
45
|
+
* @returns {Promise<object>} Merged inputs (existing + prompted)
|
|
46
|
+
*/
|
|
47
|
+
async function promptForInputs(definition, existingInputs) {
|
|
48
|
+
const { buildInputSteps } = require('../lib/workflow');
|
|
49
|
+
const { createCLIRenderer } = require('../lib/wizard-cli');
|
|
50
|
+
const { runWizard } = require('../lib/wizard');
|
|
51
|
+
|
|
52
|
+
const allSteps = buildInputSteps(definition);
|
|
53
|
+
// Only prompt for inputs not already provided
|
|
54
|
+
const steps = allSteps.filter(s => !(s.id in existingInputs));
|
|
55
|
+
if (steps.length === 0) return existingInputs;
|
|
56
|
+
|
|
57
|
+
const renderer = createCLIRenderer({
|
|
58
|
+
title: `${definition.name || 'Workflow'} inputs`,
|
|
59
|
+
doneMessage: 'Inputs ready.',
|
|
60
|
+
showBackHint: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const { answers, cancelled } = await runWizard({
|
|
64
|
+
steps,
|
|
65
|
+
config: {},
|
|
66
|
+
renderer,
|
|
67
|
+
initial: {},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (cancelled) {
|
|
71
|
+
console.log(pc.dim('Cancelled.'));
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { ...existingInputs, ...answers };
|
|
76
|
+
}
|
|
77
|
+
|
|
25
78
|
/**
|
|
26
79
|
* Register the workflow command on a Commander program.
|
|
27
80
|
* @param {import('commander').Command} program
|
|
@@ -43,12 +96,33 @@ function registerWorkflow(program) {
|
|
|
43
96
|
.option('--quiet', 'Suppress progress output', false)
|
|
44
97
|
.option('--dry-run', 'Show execution plan without running', false)
|
|
45
98
|
.option('--verbose', 'Show step details', false)
|
|
99
|
+
.option('--no-interactive', 'Disable interactive input prompting')
|
|
46
100
|
.action(async (file, opts) => {
|
|
47
|
-
const {
|
|
101
|
+
const { executeWorkflow, buildExecutionPlan, validateWorkflow } = require('../lib/workflow');
|
|
102
|
+
const { resolveWorkflow } = require('../lib/workflow-registry');
|
|
48
103
|
|
|
49
104
|
let definition;
|
|
50
105
|
try {
|
|
51
|
-
|
|
106
|
+
const resolved = resolveWorkflow(file);
|
|
107
|
+
definition = resolved.definition;
|
|
108
|
+
|
|
109
|
+
// Show workflow source notice
|
|
110
|
+
if ((resolved.source === 'community' || resolved.source === 'official') && !opts.quiet) {
|
|
111
|
+
const pkg = resolved.metadata?.package;
|
|
112
|
+
const author = typeof pkg?.author === 'string' ? pkg.author : pkg?.author?.name || 'unknown';
|
|
113
|
+
const tools = (pkg?.vai?.tools || []).join(', ');
|
|
114
|
+
const isOfficial = resolved.source === 'official';
|
|
115
|
+
const label = isOfficial ? 'official catalog workflow' : 'community workflow';
|
|
116
|
+
console.error(`${pc.dim('ℹ')} Running ${label}: ${pc.cyan(pkg?.name || file)} ${pc.dim(`v${pkg?.version || '?'}`)}`);
|
|
117
|
+
console.error(` ${pc.dim(`by ${author}`)}${tools ? pc.dim(` | Tools: ${tools}`) : ''}`);
|
|
118
|
+
if (!isOfficial) {
|
|
119
|
+
console.error(` ${pc.dim('This is a community-contributed workflow, not maintained by the vai project.')}`);
|
|
120
|
+
}
|
|
121
|
+
console.error();
|
|
122
|
+
for (const w of resolved.metadata?.warnings || []) {
|
|
123
|
+
console.error(` ${pc.yellow('⚠')} ${w}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
52
126
|
} catch (err) {
|
|
53
127
|
console.error(ui.error(err.message));
|
|
54
128
|
process.exit(1);
|
|
@@ -64,6 +138,11 @@ function registerWorkflow(program) {
|
|
|
64
138
|
|
|
65
139
|
const workflowName = definition.name || file;
|
|
66
140
|
|
|
141
|
+
// Interactive prompting for missing inputs
|
|
142
|
+
if (opts.interactive !== false && process.stdin.isTTY) {
|
|
143
|
+
opts.input = await promptForInputs(definition, opts.input);
|
|
144
|
+
}
|
|
145
|
+
|
|
67
146
|
if (opts.dryRun) {
|
|
68
147
|
// Dry run: show plan
|
|
69
148
|
const layers = buildExecutionPlan(definition.steps);
|
|
@@ -220,27 +299,396 @@ function registerWorkflow(program) {
|
|
|
220
299
|
// ── workflow list ──
|
|
221
300
|
wfCmd
|
|
222
301
|
.command('list')
|
|
223
|
-
.description('List built-in
|
|
224
|
-
.
|
|
225
|
-
|
|
302
|
+
.description('List available workflows (built-in + official + community)')
|
|
303
|
+
.option('--built-in', 'Show only built-in workflows', false)
|
|
304
|
+
.option('--official', 'Show only official @vaicli workflows', false)
|
|
305
|
+
.option('--community', 'Show only community workflows', false)
|
|
306
|
+
.option('--category <name>', 'Filter by category')
|
|
307
|
+
.option('--tag <name>', 'Filter by tag')
|
|
308
|
+
.option('--json', 'Output JSON', false)
|
|
309
|
+
.action((opts) => {
|
|
310
|
+
const { getRegistry } = require('../lib/workflow-registry');
|
|
311
|
+
const registry = getRegistry({ force: true });
|
|
312
|
+
|
|
313
|
+
const showBuiltIn = !opts.community && !opts.official;
|
|
314
|
+
const showOfficial = !opts.builtIn && !opts.community;
|
|
315
|
+
const showCommunity = !opts.builtIn && !opts.official;
|
|
226
316
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
const out = {};
|
|
319
|
+
if (showBuiltIn) out.builtIn = registry.builtIn;
|
|
320
|
+
if (showOfficial) out.official = registry.official.filter(c => c.errors.length === 0);
|
|
321
|
+
if (showCommunity) out.community = registry.community.filter(c => c.errors.length === 0);
|
|
322
|
+
console.log(JSON.stringify(out, null, 2));
|
|
230
323
|
return;
|
|
231
324
|
}
|
|
232
325
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Display a list of package-based workflows (official or community).
|
|
328
|
+
*/
|
|
329
|
+
function displayPackageList(items, label, emptyHint) {
|
|
330
|
+
let filtered = items.filter(c => c.errors.length === 0);
|
|
331
|
+
if (opts.category) {
|
|
332
|
+
filtered = filtered.filter(c => (c.pkg?.vai?.category || 'utility') === opts.category);
|
|
333
|
+
}
|
|
334
|
+
if (opts.tag) {
|
|
335
|
+
filtered = filtered.filter(c => (c.pkg?.vai?.tags || []).includes(opts.tag));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log();
|
|
339
|
+
console.log(pc.bold(`${label} (${filtered.length})`));
|
|
340
|
+
if (filtered.length === 0) {
|
|
341
|
+
console.log(pc.dim(` (${emptyHint})`));
|
|
342
|
+
} else {
|
|
343
|
+
for (const wf of filtered) {
|
|
344
|
+
const pkg = wf.pkg || {};
|
|
345
|
+
const author = typeof pkg.author === 'string' ? pkg.author : pkg.author?.name || '';
|
|
346
|
+
const tags = (pkg.vai?.tags || []).join(' · ');
|
|
347
|
+
console.log(` ${pc.cyan(wf.name.padEnd(42))} ${pkg.description || ''}`);
|
|
348
|
+
if (author || tags) {
|
|
349
|
+
console.log(` ${pc.dim(`by ${author}`)}${pkg.version ? pc.dim(` | v${pkg.version}`) : ''}${tags ? pc.dim(` | ${tags}`) : ''}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Show invalid packages as warnings
|
|
355
|
+
const invalid = items.filter(c => c.errors.length > 0);
|
|
356
|
+
if (invalid.length > 0) {
|
|
357
|
+
console.log();
|
|
358
|
+
for (const inv of invalid) {
|
|
359
|
+
console.error(` ${pc.yellow('⚠')} ${inv.name}: ${inv.errors[0]}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Built-in
|
|
365
|
+
if (showBuiltIn) {
|
|
366
|
+
console.log();
|
|
367
|
+
console.log(pc.bold(`Built-in Workflows (${registry.builtIn.length})`));
|
|
368
|
+
if (registry.builtIn.length === 0) {
|
|
369
|
+
console.log(pc.dim(' (none)'));
|
|
370
|
+
} else {
|
|
371
|
+
for (const wf of registry.builtIn) {
|
|
372
|
+
console.log(` ${pc.cyan(wf.name.padEnd(28))} ${wf.description}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Official Catalog (@vaicli)
|
|
378
|
+
if (showOfficial) {
|
|
379
|
+
displayPackageList(registry.official, 'Official Catalog (@vaicli)', 'none installed');
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Community
|
|
383
|
+
if (showCommunity) {
|
|
384
|
+
displayPackageList(registry.community, 'Community Workflows', 'none installed — install with: vai workflow install <package-name>');
|
|
238
385
|
}
|
|
386
|
+
|
|
239
387
|
console.log();
|
|
240
388
|
console.log(pc.dim('Run with: vai workflow run <name> --input key=value'));
|
|
241
389
|
console.log();
|
|
242
390
|
});
|
|
243
391
|
|
|
392
|
+
// ── workflow install ──
|
|
393
|
+
wfCmd
|
|
394
|
+
.command('install <package>')
|
|
395
|
+
.description('Install a workflow from npm')
|
|
396
|
+
.option('--global', 'Install globally', false)
|
|
397
|
+
.option('--json', 'Output JSON', false)
|
|
398
|
+
.action(async (packageName, opts) => {
|
|
399
|
+
const { installPackage, WORKFLOW_PREFIX, isWorkflowPackage, isOfficialPackage } = require('../lib/npm-utils');
|
|
400
|
+
const { validatePackage, clearRegistryCache } = require('../lib/workflow-registry');
|
|
401
|
+
|
|
402
|
+
// Auto-prefix if needed (but not for scoped packages)
|
|
403
|
+
if (!packageName.startsWith('@') && !packageName.startsWith(WORKFLOW_PREFIX)) {
|
|
404
|
+
packageName = WORKFLOW_PREFIX + packageName;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log(`Installing ${pc.cyan(packageName)}...`);
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const result = installPackage(packageName, { global: opts.global });
|
|
411
|
+
console.log(`${pc.green('✔')} Downloaded ${pc.cyan(packageName)}@${result.version}`);
|
|
412
|
+
|
|
413
|
+
// Validate
|
|
414
|
+
if (result.path) {
|
|
415
|
+
const validation = validatePackage(result.path);
|
|
416
|
+
if (validation.errors.length === 0) {
|
|
417
|
+
const steps = validation.definition?.steps?.length || 0;
|
|
418
|
+
const tools = (validation.pkg?.vai?.tools || []).join(', ');
|
|
419
|
+
console.log(`${pc.green('✔')} Validated workflow definition (${steps} steps${tools ? `, tools: ${tools}` : ''})`);
|
|
420
|
+
} else {
|
|
421
|
+
console.log(`${pc.yellow('⚠')} Validation issues:`);
|
|
422
|
+
for (const e of validation.errors) {
|
|
423
|
+
console.log(` ${pc.yellow('-')} ${e}`);
|
|
424
|
+
}
|
|
425
|
+
console.log();
|
|
426
|
+
console.log(pc.dim('The package was installed but the workflow may not execute correctly.'));
|
|
427
|
+
}
|
|
428
|
+
for (const w of validation.warnings) {
|
|
429
|
+
console.log(`${pc.yellow('⚠')} ${w}`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
clearRegistryCache();
|
|
434
|
+
|
|
435
|
+
if (opts.json) {
|
|
436
|
+
console.log(JSON.stringify(result, null, 2));
|
|
437
|
+
} else {
|
|
438
|
+
console.log();
|
|
439
|
+
console.log(`Installed. Run with:`);
|
|
440
|
+
console.log(` ${pc.cyan(`vai workflow run ${packageName}`)} --input key=value`);
|
|
441
|
+
}
|
|
442
|
+
} catch (err) {
|
|
443
|
+
console.error(ui.error(err.message));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// ── workflow uninstall ──
|
|
449
|
+
wfCmd
|
|
450
|
+
.command('uninstall <package>')
|
|
451
|
+
.description('Remove a workflow package')
|
|
452
|
+
.option('--global', 'Uninstall globally', false)
|
|
453
|
+
.action((packageName, opts) => {
|
|
454
|
+
const { uninstallPackage, WORKFLOW_PREFIX } = require('../lib/npm-utils');
|
|
455
|
+
const { clearRegistryCache } = require('../lib/workflow-registry');
|
|
456
|
+
|
|
457
|
+
if (!packageName.startsWith('@') && !packageName.startsWith(WORKFLOW_PREFIX)) {
|
|
458
|
+
packageName = WORKFLOW_PREFIX + packageName;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
console.log(`Uninstalling ${pc.cyan(packageName)}...`);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
uninstallPackage(packageName, { global: opts.global });
|
|
465
|
+
clearRegistryCache();
|
|
466
|
+
console.log(`${pc.green('✔')} Removed ${packageName}`);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.error(ui.error(err.message));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// ── workflow search ──
|
|
474
|
+
wfCmd
|
|
475
|
+
.command('search <query>')
|
|
476
|
+
.description('Search npm for community workflows')
|
|
477
|
+
.option('--limit <n>', 'Maximum results', '10')
|
|
478
|
+
.option('--json', 'Output JSON', false)
|
|
479
|
+
.action(async (query, opts) => {
|
|
480
|
+
const { searchNpm } = require('../lib/npm-utils');
|
|
481
|
+
|
|
482
|
+
console.log(`Searching npm for vai-workflow packages matching "${query}"...`);
|
|
483
|
+
console.log();
|
|
484
|
+
|
|
485
|
+
try {
|
|
486
|
+
const results = await searchNpm(query, { limit: parseInt(opts.limit, 10) });
|
|
487
|
+
|
|
488
|
+
if (opts.json) {
|
|
489
|
+
console.log(JSON.stringify(results, null, 2));
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (results.length === 0) {
|
|
494
|
+
console.log(pc.dim(' No matching workflow packages found.'));
|
|
495
|
+
console.log();
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
for (const r of results) {
|
|
500
|
+
const badge = r.official ? ` ${pc.green('[OFFICIAL]')}` : '';
|
|
501
|
+
console.log(` ${pc.cyan(r.name)} ${pc.dim(`v${r.version}`)}${badge}`);
|
|
502
|
+
if (r.description) console.log(` ${r.description}`);
|
|
503
|
+
console.log(` ${pc.dim(`by ${r.author}`)}${r.keywords.length ? pc.dim(` | ${r.keywords.slice(0, 5).join(', ')}`) : ''}`);
|
|
504
|
+
console.log();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log(pc.dim(`Install: vai workflow install <package-name>`));
|
|
508
|
+
console.log();
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.error(ui.error(err.message));
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ── workflow info ──
|
|
516
|
+
wfCmd
|
|
517
|
+
.command('info <name>')
|
|
518
|
+
.description('Show detailed info about an installed workflow')
|
|
519
|
+
.option('--json', 'Output JSON', false)
|
|
520
|
+
.action((name, opts) => {
|
|
521
|
+
const { resolveWorkflow, getRegistry } = require('../lib/workflow-registry');
|
|
522
|
+
const { WORKFLOW_PREFIX } = require('../lib/npm-utils');
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
const resolved = resolveWorkflow(name);
|
|
526
|
+
|
|
527
|
+
if (opts.json) {
|
|
528
|
+
console.log(JSON.stringify({ source: resolved.source, definition: resolved.definition, metadata: resolved.metadata }, null, 2));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const def = resolved.definition;
|
|
533
|
+
console.log();
|
|
534
|
+
|
|
535
|
+
if (resolved.source === 'community' || resolved.source === 'official') {
|
|
536
|
+
const pkg = resolved.metadata?.package || {};
|
|
537
|
+
const author = typeof pkg.author === 'string' ? pkg.author : pkg.author?.name || 'unknown';
|
|
538
|
+
const vai = pkg.vai || {};
|
|
539
|
+
|
|
540
|
+
console.log(`${pc.bold(pc.cyan(pkg.name || name))} ${pc.dim(`v${pkg.version || '?'}`)}`);
|
|
541
|
+
console.log(` ${pkg.description || def.description || ''}`);
|
|
542
|
+
console.log();
|
|
543
|
+
console.log(` ${pc.dim('Author:')} ${author}`);
|
|
544
|
+
console.log(` ${pc.dim('License:')} ${pkg.license || 'unknown'}`);
|
|
545
|
+
console.log(` ${pc.dim('Category:')} ${vai.category || 'utility'}`);
|
|
546
|
+
if (vai.tags?.length) console.log(` ${pc.dim('Tags:')} ${vai.tags.join(', ')}`);
|
|
547
|
+
if (vai.minVaiVersion) console.log(` ${pc.dim('Min vai:')} v${vai.minVaiVersion}`);
|
|
548
|
+
if (vai.tools?.length) console.log(` ${pc.dim('Tools:')} ${vai.tools.join(', ')}`);
|
|
549
|
+
console.log(` ${pc.dim('Steps:')} ${def.steps?.length || 0}`);
|
|
550
|
+
console.log(` ${pc.dim('Source:')} ${resolved.metadata?.path || 'unknown'}`);
|
|
551
|
+
if (pkg.name) console.log(` ${pc.dim('npm:')} https://www.npmjs.com/package/${pkg.name}`);
|
|
552
|
+
} else {
|
|
553
|
+
console.log(`${pc.bold(pc.cyan(def.name || name))} ${pc.dim(`[${resolved.source}]`)}`);
|
|
554
|
+
console.log(` ${def.description || ''}`);
|
|
555
|
+
console.log(` ${pc.dim('Steps:')} ${def.steps?.length || 0}`);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Show inputs
|
|
559
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
560
|
+
console.log();
|
|
561
|
+
console.log(` ${pc.bold('Inputs:')}`);
|
|
562
|
+
for (const [key, schema] of Object.entries(def.inputs)) {
|
|
563
|
+
const req = schema.required ? pc.red('(required)') : pc.dim(`(default: ${schema.default ?? 'none'})`);
|
|
564
|
+
const desc = schema.description || '';
|
|
565
|
+
console.log(` ${pc.cyan(key.padEnd(16))} ${(schema.type || 'string').padEnd(8)} ${req} ${pc.dim(desc)}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
console.log();
|
|
570
|
+
} catch (err) {
|
|
571
|
+
console.error(ui.error(err.message));
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// ── workflow create ──
|
|
577
|
+
wfCmd
|
|
578
|
+
.command('create')
|
|
579
|
+
.description('Scaffold a publish-ready npm package from a workflow')
|
|
580
|
+
.option('--from <file>', 'Existing workflow JSON to package')
|
|
581
|
+
.option('--name <name>', 'Package name (without vai-workflow- prefix)')
|
|
582
|
+
.option('--author <name>', 'Author name')
|
|
583
|
+
.option('--description <desc>', 'Package description')
|
|
584
|
+
.option('--category <cat>', 'Category (retrieval, analysis, ingestion, domain-specific, utility, integration)')
|
|
585
|
+
.option('--scope <scope>', 'Package scope (e.g. "vaicli" for @vaicli/vai-workflow-*)')
|
|
586
|
+
.option('--output <dir>', 'Output directory')
|
|
587
|
+
.action(async (opts) => {
|
|
588
|
+
const { scaffoldPackage, toPackageName, CATEGORIES, emptyWorkflowTemplate } = require('../lib/workflow-scaffold');
|
|
589
|
+
const { loadWorkflow } = require('../lib/workflow');
|
|
590
|
+
|
|
591
|
+
let definition;
|
|
592
|
+
let name = opts.name;
|
|
593
|
+
let author = opts.author;
|
|
594
|
+
let description = opts.description;
|
|
595
|
+
let category = opts.category;
|
|
596
|
+
|
|
597
|
+
if (opts.from) {
|
|
598
|
+
// Package an existing workflow
|
|
599
|
+
try {
|
|
600
|
+
definition = loadWorkflow(opts.from);
|
|
601
|
+
} catch (err) {
|
|
602
|
+
console.error(ui.error(err.message));
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
if (!name) {
|
|
606
|
+
name = definition.name || path.basename(opts.from, '.json').replace('.vai-workflow', '');
|
|
607
|
+
}
|
|
608
|
+
if (!description) {
|
|
609
|
+
description = definition.description;
|
|
610
|
+
}
|
|
611
|
+
} else if (process.stdin.isTTY) {
|
|
612
|
+
// Interactive mode
|
|
613
|
+
try {
|
|
614
|
+
const p = require('@clack/prompts');
|
|
615
|
+
p.intro(pc.bold('Create a new workflow package'));
|
|
616
|
+
|
|
617
|
+
const answers = await p.group({
|
|
618
|
+
name: () => p.text({ message: 'Workflow name', placeholder: 'my-workflow', validate: v => v ? undefined : 'Required' }),
|
|
619
|
+
description: () => p.text({ message: 'Description', placeholder: 'A brief description of what this workflow does' }),
|
|
620
|
+
category: () => p.select({
|
|
621
|
+
message: 'Category',
|
|
622
|
+
options: CATEGORIES.map(c => ({ value: c, label: c })),
|
|
623
|
+
}),
|
|
624
|
+
author: () => p.text({ message: 'Author', placeholder: 'Your Name', defaultValue: getGitAuthor() }),
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
if (p.isCancel(answers)) {
|
|
628
|
+
p.cancel('Cancelled.');
|
|
629
|
+
process.exit(0);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
name = answers.name;
|
|
633
|
+
description = answers.description;
|
|
634
|
+
category = answers.category;
|
|
635
|
+
author = answers.author;
|
|
636
|
+
definition = emptyWorkflowTemplate();
|
|
637
|
+
definition.name = name;
|
|
638
|
+
definition.description = description || '';
|
|
639
|
+
// Add a placeholder step so validation passes
|
|
640
|
+
definition.steps = [{
|
|
641
|
+
id: 'search',
|
|
642
|
+
tool: 'query',
|
|
643
|
+
name: 'Search',
|
|
644
|
+
inputs: { query: '{{ inputs.query }}' },
|
|
645
|
+
}];
|
|
646
|
+
definition.inputs = {
|
|
647
|
+
query: { type: 'string', required: true, description: 'Search query' },
|
|
648
|
+
};
|
|
649
|
+
} catch (err) {
|
|
650
|
+
console.error(ui.error(`Interactive mode failed: ${err.message}`));
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
console.error(ui.error('Provide --from <file> or run interactively (TTY required).'));
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (!name) {
|
|
659
|
+
console.error(ui.error('Workflow name is required. Use --name <name>.'));
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
const result = scaffoldPackage({
|
|
665
|
+
definition,
|
|
666
|
+
name,
|
|
667
|
+
author,
|
|
668
|
+
description,
|
|
669
|
+
category,
|
|
670
|
+
scope: opts.scope,
|
|
671
|
+
outputDir: opts.output,
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const pkgName = toPackageName(name, { scope: opts.scope });
|
|
675
|
+
console.log();
|
|
676
|
+
console.log(`${pc.green('✔')} Created ${pc.cyan(pkgName)}/`);
|
|
677
|
+
for (const f of result.files) {
|
|
678
|
+
console.log(` ${pc.dim('├──')} ${f}`);
|
|
679
|
+
}
|
|
680
|
+
console.log();
|
|
681
|
+
console.log('Next steps:');
|
|
682
|
+
console.log(` 1. ${opts.from ? '' : pc.dim('Edit workflow.json with your workflow definition')}${opts.from ? 'Review README.md' : ''}`);
|
|
683
|
+
console.log(` 2. cd ${pkgName}`);
|
|
684
|
+
console.log(` 3. npm publish`);
|
|
685
|
+
console.log();
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.error(ui.error(err.message));
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
244
692
|
// ── workflow init ──
|
|
245
693
|
wfCmd
|
|
246
694
|
.command('init')
|
package/src/lib/api.js
CHANGED
|
@@ -4,10 +4,24 @@ const ATLAS_API_BASE = 'https://ai.mongodb.com/v1';
|
|
|
4
4
|
const VOYAGE_API_BASE = 'https://api.voyageai.com/v1';
|
|
5
5
|
const MAX_RETRIES = 3;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Identify the key type from its prefix.
|
|
9
|
+
* @param {string} key
|
|
10
|
+
* @returns {{ type: 'atlas'|'voyage'|'unknown', label: string, expectedBase: string }}
|
|
11
|
+
*/
|
|
12
|
+
function identifyKey(key) {
|
|
13
|
+
if (key.startsWith('al-')) {
|
|
14
|
+
return { type: 'atlas', label: 'MongoDB Atlas', expectedBase: ATLAS_API_BASE };
|
|
15
|
+
}
|
|
16
|
+
if (key.startsWith('pa-')) {
|
|
17
|
+
return { type: 'voyage', label: 'Voyage AI (direct)', expectedBase: VOYAGE_API_BASE };
|
|
18
|
+
}
|
|
19
|
+
return { type: 'unknown', label: 'Unknown provider', expectedBase: ATLAS_API_BASE };
|
|
20
|
+
}
|
|
21
|
+
|
|
7
22
|
/**
|
|
8
23
|
* Resolve the API base URL.
|
|
9
24
|
* Priority: VOYAGE_API_BASE env → config baseUrl → auto-detect from key prefix.
|
|
10
|
-
* Keys starting with 'pa-' that work on Voyage platform use VOYAGE_API_BASE.
|
|
11
25
|
* @returns {string}
|
|
12
26
|
*/
|
|
13
27
|
function getApiBase() {
|
|
@@ -20,6 +34,10 @@ function getApiBase() {
|
|
|
20
34
|
const configBase = getConfigValue('baseUrl');
|
|
21
35
|
if (configBase) return configBase.replace(/\/+$/, '');
|
|
22
36
|
|
|
37
|
+
// Auto-detect from key prefix
|
|
38
|
+
const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
|
|
39
|
+
if (key) return identifyKey(key).expectedBase;
|
|
40
|
+
|
|
23
41
|
// Default to Atlas endpoint
|
|
24
42
|
return ATLAS_API_BASE;
|
|
25
43
|
}
|
|
@@ -29,7 +47,7 @@ const API_BASE = ATLAS_API_BASE;
|
|
|
29
47
|
|
|
30
48
|
/**
|
|
31
49
|
* Get the Voyage API key or exit with a helpful error.
|
|
32
|
-
*
|
|
50
|
+
* Validates that the key prefix matches the configured base URL and warns on mismatch.
|
|
33
51
|
* @returns {string}
|
|
34
52
|
*/
|
|
35
53
|
function requireApiKey() {
|
|
@@ -43,6 +61,25 @@ function requireApiKey() {
|
|
|
43
61
|
' or Voyage AI platform > Dashboard > API Keys';
|
|
44
62
|
throw new Error(msg);
|
|
45
63
|
}
|
|
64
|
+
|
|
65
|
+
// Validate key/endpoint match and warn on mismatch
|
|
66
|
+
const base = getApiBase();
|
|
67
|
+
const keyInfo = identifyKey(key);
|
|
68
|
+
|
|
69
|
+
if (keyInfo.type !== 'unknown' && keyInfo.expectedBase !== base) {
|
|
70
|
+
const mismatch =
|
|
71
|
+
`\n⚠️ API key/endpoint mismatch detected!\n` +
|
|
72
|
+
` Key type: ${keyInfo.label} (${key.slice(0, 5)}...)\n` +
|
|
73
|
+
` Endpoint: ${base}\n` +
|
|
74
|
+
` Expected: ${keyInfo.expectedBase}\n\n` +
|
|
75
|
+
` This will likely cause a 401 or 403 error.\n\n` +
|
|
76
|
+
` Fix: Update your base URL to match your key:\n` +
|
|
77
|
+
` vai config set base-url ${keyInfo.expectedBase}\n\n` +
|
|
78
|
+
` Or switch to a ${base.includes('ai.mongodb.com') ? 'MongoDB Atlas' : 'Voyage AI'} key:\n` +
|
|
79
|
+
` vai config set api-key <your-${base.includes('ai.mongodb.com') ? 'atlas' : 'voyage'}-key>\n`;
|
|
80
|
+
process.stderr.write(mismatch);
|
|
81
|
+
}
|
|
82
|
+
|
|
46
83
|
return key;
|
|
47
84
|
}
|
|
48
85
|
|
|
@@ -162,6 +199,7 @@ module.exports = {
|
|
|
162
199
|
API_BASE,
|
|
163
200
|
ATLAS_API_BASE,
|
|
164
201
|
VOYAGE_API_BASE,
|
|
202
|
+
identifyKey,
|
|
165
203
|
getApiBase,
|
|
166
204
|
requireApiKey,
|
|
167
205
|
apiRequest,
|