voyageai-cli 1.28.0 → 1.30.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 +82 -8
- package/package.json +2 -1
- package/src/commands/app.js +15 -0
- package/src/commands/benchmark.js +22 -8
- package/src/commands/chat.js +18 -0
- package/src/commands/chunk.js +10 -0
- package/src/commands/demo.js +4 -0
- package/src/commands/embed.js +13 -0
- package/src/commands/estimate.js +3 -0
- package/src/commands/eval.js +6 -0
- package/src/commands/explain.js +2 -0
- package/src/commands/generate.js +2 -0
- package/src/commands/ingest.js +4 -0
- package/src/commands/init.js +2 -0
- package/src/commands/mcp-server.js +2 -0
- package/src/commands/models.js +2 -0
- package/src/commands/ping.js +7 -0
- package/src/commands/pipeline.js +15 -0
- package/src/commands/playground.js +685 -8
- package/src/commands/query.js +16 -0
- package/src/commands/rerank.js +12 -0
- package/src/commands/scaffold.js +2 -0
- package/src/commands/search.js +11 -0
- package/src/commands/similarity.js +9 -0
- package/src/commands/store.js +4 -0
- package/src/commands/workflow.js +702 -13
- package/src/lib/capability-report.js +134 -0
- package/src/lib/chat.js +32 -1
- package/src/lib/config.js +2 -0
- package/src/lib/cost-display.js +107 -0
- package/src/lib/explanations.js +94 -0
- package/src/lib/llm.js +125 -18
- package/src/lib/npm-utils.js +265 -0
- package/src/lib/quality-audit.js +71 -0
- package/src/lib/security/blocked-domains.json +17 -0
- package/src/lib/security-audit.js +198 -0
- package/src/lib/telemetry.js +23 -1
- package/src/lib/workflow-registry.js +416 -0
- package/src/lib/workflow-scaffold.js +380 -0
- package/src/lib/workflow-test-runner.js +208 -0
- package/src/lib/workflow.js +559 -7
- package/src/playground/announcements.md +80 -0
- package/src/playground/assets/announcements/appstore.jpg +0 -0
- package/src/playground/assets/announcements/circuits.jpg +0 -0
- package/src/playground/assets/announcements/csvingest.jpg +0 -0
- package/src/playground/assets/announcements/green-wave.jpg +0 -0
- package/src/playground/help/workflow-nodes.js +472 -0
- package/src/playground/icons/V.png +0 -0
- package/src/playground/index.html +3634 -226
- 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.
|
|
@@ -84,11 +98,31 @@ function registerWorkflow(program) {
|
|
|
84
98
|
.option('--verbose', 'Show step details', false)
|
|
85
99
|
.option('--no-interactive', 'Disable interactive input prompting')
|
|
86
100
|
.action(async (file, opts) => {
|
|
87
|
-
const {
|
|
101
|
+
const { executeWorkflow, buildExecutionPlan, validateWorkflow } = require('../lib/workflow');
|
|
102
|
+
const { resolveWorkflow } = require('../lib/workflow-registry');
|
|
88
103
|
|
|
89
104
|
let definition;
|
|
90
105
|
try {
|
|
91
|
-
|
|
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
|
+
}
|
|
92
126
|
} catch (err) {
|
|
93
127
|
console.error(ui.error(err.message));
|
|
94
128
|
process.exit(1);
|
|
@@ -154,6 +188,12 @@ function registerWorkflow(program) {
|
|
|
154
188
|
}
|
|
155
189
|
|
|
156
190
|
// Execute workflow
|
|
191
|
+
const telemetry = require('../lib/telemetry');
|
|
192
|
+
const wfDone = telemetry.timer('cli_workflow_run', {
|
|
193
|
+
workflowName,
|
|
194
|
+
stepCount: definition.steps?.length || 0,
|
|
195
|
+
isBuiltin: !!(definition._source === 'builtin'),
|
|
196
|
+
});
|
|
157
197
|
try {
|
|
158
198
|
const result = await executeWorkflow(definition, {
|
|
159
199
|
inputs: opts.input,
|
|
@@ -190,6 +230,8 @@ function registerWorkflow(program) {
|
|
|
190
230
|
console.error();
|
|
191
231
|
}
|
|
192
232
|
|
|
233
|
+
wfDone();
|
|
234
|
+
|
|
193
235
|
// Output
|
|
194
236
|
if (opts.json) {
|
|
195
237
|
console.log(JSON.stringify(result.output, null, 2));
|
|
@@ -227,6 +269,269 @@ function registerWorkflow(program) {
|
|
|
227
269
|
}
|
|
228
270
|
});
|
|
229
271
|
|
|
272
|
+
// ── workflow check <path> ──
|
|
273
|
+
wfCmd
|
|
274
|
+
.command('check <path>')
|
|
275
|
+
.description('Run validation, security, and quality checks on a workflow package')
|
|
276
|
+
.option('--security', 'Run security checks only', false)
|
|
277
|
+
.option('--quality', 'Run quality checks only', false)
|
|
278
|
+
.option('--all', 'Run all check tiers', false)
|
|
279
|
+
.option('--json', 'Output machine-readable JSON', false)
|
|
280
|
+
.option('--ci', 'Output CI-optimized JSON with summary metadata', false)
|
|
281
|
+
.action((pkgPath, opts) => {
|
|
282
|
+
const { validateSchemaEnhanced, loadWorkflow } = require('../lib/workflow');
|
|
283
|
+
const { securityAudit, extractCapabilities } = require('../lib/security-audit');
|
|
284
|
+
const { qualityAudit } = require('../lib/quality-audit');
|
|
285
|
+
const fs = require('fs');
|
|
286
|
+
|
|
287
|
+
const resolvedPath = path.resolve(pkgPath);
|
|
288
|
+
const runAll = opts.all || (!opts.security && !opts.quality);
|
|
289
|
+
|
|
290
|
+
// Load workflow definition
|
|
291
|
+
let definition;
|
|
292
|
+
let pkg = {};
|
|
293
|
+
let workflowFile;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
// Check if it's a directory (package) or a single file
|
|
297
|
+
const stat = fs.statSync(resolvedPath);
|
|
298
|
+
if (stat.isDirectory()) {
|
|
299
|
+
// Package directory
|
|
300
|
+
const pkgJsonPath = path.join(resolvedPath, 'package.json');
|
|
301
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
302
|
+
pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
303
|
+
}
|
|
304
|
+
// Find workflow file
|
|
305
|
+
workflowFile = pkg.main || 'workflow.json';
|
|
306
|
+
const wfPath = path.join(resolvedPath, workflowFile);
|
|
307
|
+
if (fs.existsSync(wfPath)) {
|
|
308
|
+
definition = JSON.parse(fs.readFileSync(wfPath, 'utf8'));
|
|
309
|
+
} else {
|
|
310
|
+
console.error(ui.error(`Workflow file not found: ${wfPath}`));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
// Single file
|
|
315
|
+
definition = loadWorkflow(resolvedPath);
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(ui.error(err.message));
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const results = { schema: [], security: [], quality: [], capabilities: [] };
|
|
323
|
+
|
|
324
|
+
// L1: Schema validation (always runs)
|
|
325
|
+
if (runAll || (!opts.security && !opts.quality)) {
|
|
326
|
+
results.schema = validateSchemaEnhanced(definition);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// L2: Security
|
|
330
|
+
if (runAll || opts.security) {
|
|
331
|
+
const packageDir = fs.statSync(resolvedPath).isDirectory() ? resolvedPath : null;
|
|
332
|
+
results.security = securityAudit(definition, packageDir);
|
|
333
|
+
results.capabilities = [...extractCapabilities(definition)];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// L3: Quality
|
|
337
|
+
if (runAll || opts.quality) {
|
|
338
|
+
const packageDir = fs.statSync(resolvedPath).isDirectory() ? resolvedPath : null;
|
|
339
|
+
results.quality = qualityAudit(definition, pkg, packageDir);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// CI output (machine-readable JSON with summary metadata)
|
|
343
|
+
if (opts.ci) {
|
|
344
|
+
const criticalCount = results.security.filter(f => f.severity === 'critical').length;
|
|
345
|
+
const highCount = results.security.filter(f => f.severity === 'high').length;
|
|
346
|
+
const schemaPass = results.schema.length === 0;
|
|
347
|
+
const securityPass = criticalCount === 0 && highCount === 0;
|
|
348
|
+
const qualityPass = results.quality.filter(i => i.level === 'error').length === 0;
|
|
349
|
+
const ciOutput = {
|
|
350
|
+
schema: { pass: schemaPass, errors: results.schema },
|
|
351
|
+
security: { pass: securityPass, findings: results.security },
|
|
352
|
+
quality: { pass: qualityPass, issues: results.quality },
|
|
353
|
+
capabilities: results.capabilities,
|
|
354
|
+
summary: {
|
|
355
|
+
passAll: schemaPass && securityPass && qualityPass,
|
|
356
|
+
criticalCount,
|
|
357
|
+
highCount,
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
console.log(JSON.stringify(ciOutput, null, 2));
|
|
361
|
+
if (criticalCount > 0) process.exit(2);
|
|
362
|
+
if (highCount > 0) process.exit(1);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// JSON output
|
|
367
|
+
if (opts.json) {
|
|
368
|
+
console.log(JSON.stringify(results, null, 2));
|
|
369
|
+
// Exit code 1 if critical/high security findings
|
|
370
|
+
const hasCritical = results.security.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
371
|
+
if (hasCritical) process.exit(1);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Pretty output
|
|
376
|
+
console.log();
|
|
377
|
+
console.log(pc.bold(`Workflow Check: ${definition.name || pkgPath}`));
|
|
378
|
+
console.log(pc.dim('═'.repeat(50)));
|
|
379
|
+
|
|
380
|
+
// Schema
|
|
381
|
+
if (results.schema.length > 0) {
|
|
382
|
+
console.log();
|
|
383
|
+
console.log(pc.bold('Schema Validation:'));
|
|
384
|
+
for (const e of results.schema) {
|
|
385
|
+
console.log(` ${pc.red('✗')} ${e}`);
|
|
386
|
+
}
|
|
387
|
+
} else if (runAll || (!opts.security && !opts.quality)) {
|
|
388
|
+
console.log();
|
|
389
|
+
console.log(`${pc.bold('Schema Validation:')} ${pc.green('✔ passed')}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Security
|
|
393
|
+
if (runAll || opts.security) {
|
|
394
|
+
console.log();
|
|
395
|
+
console.log(pc.bold('Security Audit:'));
|
|
396
|
+
if (results.security.length === 0) {
|
|
397
|
+
console.log(` ${pc.green('✔')} No security issues found`);
|
|
398
|
+
} else {
|
|
399
|
+
for (const f of results.security) {
|
|
400
|
+
const color = f.severity === 'critical' ? pc.red :
|
|
401
|
+
f.severity === 'high' ? pc.red :
|
|
402
|
+
f.severity === 'medium' ? pc.yellow : pc.dim;
|
|
403
|
+
const icon = f.severity === 'critical' || f.severity === 'high' ? '✗' : '⚠';
|
|
404
|
+
console.log(` ${color(icon)} [${f.severity.toUpperCase()}] ${f.message}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Capability flags
|
|
409
|
+
if (results.capabilities.length > 0) {
|
|
410
|
+
console.log();
|
|
411
|
+
console.log(pc.bold('Capabilities:'));
|
|
412
|
+
const capIcons = { NETWORK: '🌐', WRITE_DB: '💾', LLM: '🤖', LOOP: '🔄', READ_DB: '📊' };
|
|
413
|
+
for (const cap of results.capabilities) {
|
|
414
|
+
console.log(` ${capIcons[cap] || '•'} ${cap}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Quality
|
|
420
|
+
if (runAll || opts.quality) {
|
|
421
|
+
console.log();
|
|
422
|
+
console.log(pc.bold('Quality Audit:'));
|
|
423
|
+
if (results.quality.length === 0) {
|
|
424
|
+
console.log(` ${pc.green('✔')} No quality issues found`);
|
|
425
|
+
} else {
|
|
426
|
+
for (const issue of results.quality) {
|
|
427
|
+
const color = issue.level === 'error' ? pc.red :
|
|
428
|
+
issue.level === 'warning' ? pc.yellow : pc.dim;
|
|
429
|
+
const icon = issue.level === 'error' ? '✗' : issue.level === 'warning' ? '⚠' : 'ℹ';
|
|
430
|
+
console.log(` ${color(icon)} [${issue.level.toUpperCase()}] ${issue.message}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log();
|
|
436
|
+
|
|
437
|
+
// Exit code 1 if critical/high security findings
|
|
438
|
+
const hasCritical = results.security.some(f => f.severity === 'critical' || f.severity === 'high');
|
|
439
|
+
if (hasCritical) process.exit(1);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// ── workflow test <path> ──
|
|
443
|
+
wfCmd
|
|
444
|
+
.command('test <path>')
|
|
445
|
+
.description('Run test fixtures for a workflow package')
|
|
446
|
+
.option('--test <name>', 'Run a specific test case by name')
|
|
447
|
+
.option('--json', 'Output machine-readable JSON', false)
|
|
448
|
+
.action(async (pkgPath, opts) => {
|
|
449
|
+
const { loadWorkflow } = require('../lib/workflow');
|
|
450
|
+
const { runAllTests, loadTestCases } = require('../lib/workflow-test-runner');
|
|
451
|
+
const fs = require('fs');
|
|
452
|
+
|
|
453
|
+
const resolvedPath = path.resolve(pkgPath);
|
|
454
|
+
|
|
455
|
+
// Load workflow definition
|
|
456
|
+
let definition;
|
|
457
|
+
try {
|
|
458
|
+
const stat = fs.statSync(resolvedPath);
|
|
459
|
+
if (stat.isDirectory()) {
|
|
460
|
+
const wfPath = path.join(resolvedPath, 'workflow.json');
|
|
461
|
+
if (fs.existsSync(wfPath)) {
|
|
462
|
+
definition = JSON.parse(fs.readFileSync(wfPath, 'utf8'));
|
|
463
|
+
} else {
|
|
464
|
+
console.error(ui.error(`Workflow file not found: ${wfPath}`));
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
console.error(ui.error('Path must be a workflow package directory'));
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
} catch (err) {
|
|
472
|
+
console.error(ui.error(err.message));
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Check for tests directory
|
|
477
|
+
const testsDir = path.join(resolvedPath, 'tests');
|
|
478
|
+
if (!fs.existsSync(testsDir)) {
|
|
479
|
+
console.error(ui.error('No tests/ directory found in package'));
|
|
480
|
+
process.exit(1);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const testCases = loadTestCases(resolvedPath);
|
|
484
|
+
if (testCases.length === 0) {
|
|
485
|
+
console.error(ui.error('No *.test.json files found in tests/'));
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
const aggregate = await runAllTests(definition, resolvedPath, {
|
|
491
|
+
testName: opts.test,
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (opts.json) {
|
|
495
|
+
console.log(JSON.stringify(aggregate, null, 2));
|
|
496
|
+
if (aggregate.failed > 0) process.exit(1);
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Pretty output
|
|
501
|
+
console.log();
|
|
502
|
+
console.log(pc.bold(`Workflow Tests: ${definition.name || pkgPath}`));
|
|
503
|
+
console.log(pc.dim('═'.repeat(50)));
|
|
504
|
+
console.log();
|
|
505
|
+
|
|
506
|
+
for (const result of aggregate.results) {
|
|
507
|
+
const icon = result.passed ? pc.green('✔') : pc.red('✗');
|
|
508
|
+
console.log(`${icon} ${result.name || result.file}`);
|
|
509
|
+
|
|
510
|
+
if (result.error) {
|
|
511
|
+
console.log(` ${pc.red(result.error)}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
for (const assertion of result.assertions) {
|
|
515
|
+
const aIcon = assertion.pass ? pc.green(' ✔') : pc.red(' ✗');
|
|
516
|
+
console.log(`${aIcon} ${assertion.message}`);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const err of (result.errors || [])) {
|
|
520
|
+
console.log(` ${pc.red('Error:')} ${err}`);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log();
|
|
525
|
+
console.log(`${pc.bold('Summary:')} ${pc.green(`${aggregate.passed} passed`)}, ${aggregate.failed > 0 ? pc.red(`${aggregate.failed} failed`) : `${aggregate.failed} failed`}`);
|
|
526
|
+
console.log();
|
|
527
|
+
|
|
528
|
+
if (aggregate.failed > 0) process.exit(1);
|
|
529
|
+
} catch (err) {
|
|
530
|
+
console.error(ui.error(err.message));
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
|
|
230
535
|
// ── workflow validate <file> ──
|
|
231
536
|
wfCmd
|
|
232
537
|
.command('validate <file>')
|
|
@@ -265,27 +570,411 @@ function registerWorkflow(program) {
|
|
|
265
570
|
// ── workflow list ──
|
|
266
571
|
wfCmd
|
|
267
572
|
.command('list')
|
|
268
|
-
.description('List built-in
|
|
269
|
-
.
|
|
270
|
-
|
|
573
|
+
.description('List available workflows (built-in + official + community)')
|
|
574
|
+
.option('--built-in', 'Show only built-in workflows', false)
|
|
575
|
+
.option('--official', 'Show only official @vaicli workflows', false)
|
|
576
|
+
.option('--community', 'Show only community workflows', false)
|
|
577
|
+
.option('--category <name>', 'Filter by category')
|
|
578
|
+
.option('--tag <name>', 'Filter by tag')
|
|
579
|
+
.option('--json', 'Output JSON', false)
|
|
580
|
+
.action((opts) => {
|
|
581
|
+
const { getRegistry } = require('../lib/workflow-registry');
|
|
582
|
+
const registry = getRegistry({ force: true });
|
|
583
|
+
|
|
584
|
+
const showBuiltIn = !opts.community && !opts.official;
|
|
585
|
+
const showOfficial = !opts.builtIn && !opts.community;
|
|
586
|
+
const showCommunity = !opts.builtIn && !opts.official;
|
|
271
587
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
588
|
+
if (opts.json) {
|
|
589
|
+
const out = {};
|
|
590
|
+
if (showBuiltIn) out.builtIn = registry.builtIn;
|
|
591
|
+
if (showOfficial) out.official = registry.official.filter(c => c.errors.length === 0);
|
|
592
|
+
if (showCommunity) out.community = registry.community.filter(c => c.errors.length === 0);
|
|
593
|
+
console.log(JSON.stringify(out, null, 2));
|
|
275
594
|
return;
|
|
276
595
|
}
|
|
277
596
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
597
|
+
/**
|
|
598
|
+
* Display a list of package-based workflows (official or community).
|
|
599
|
+
*/
|
|
600
|
+
function displayPackageList(items, label, emptyHint) {
|
|
601
|
+
let filtered = items.filter(c => c.errors.length === 0);
|
|
602
|
+
if (opts.category) {
|
|
603
|
+
filtered = filtered.filter(c => (c.pkg?.vai?.category || 'utility') === opts.category);
|
|
604
|
+
}
|
|
605
|
+
if (opts.tag) {
|
|
606
|
+
filtered = filtered.filter(c => (c.pkg?.vai?.tags || []).includes(opts.tag));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
console.log();
|
|
610
|
+
console.log(pc.bold(`${label} (${filtered.length})`));
|
|
611
|
+
if (filtered.length === 0) {
|
|
612
|
+
console.log(pc.dim(` (${emptyHint})`));
|
|
613
|
+
} else {
|
|
614
|
+
for (const wf of filtered) {
|
|
615
|
+
const pkg = wf.pkg || {};
|
|
616
|
+
const author = typeof pkg.author === 'string' ? pkg.author : pkg.author?.name || '';
|
|
617
|
+
const tags = (pkg.vai?.tags || []).join(' · ');
|
|
618
|
+
console.log(` ${pc.cyan(wf.name.padEnd(42))} ${pkg.description || ''}`);
|
|
619
|
+
if (author || tags) {
|
|
620
|
+
console.log(` ${pc.dim(`by ${author}`)}${pkg.version ? pc.dim(` | v${pkg.version}`) : ''}${tags ? pc.dim(` | ${tags}`) : ''}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Show invalid packages as warnings
|
|
626
|
+
const invalid = items.filter(c => c.errors.length > 0);
|
|
627
|
+
if (invalid.length > 0) {
|
|
628
|
+
console.log();
|
|
629
|
+
for (const inv of invalid) {
|
|
630
|
+
console.error(` ${pc.yellow('⚠')} ${inv.name}: ${inv.errors[0]}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
283
633
|
}
|
|
634
|
+
|
|
635
|
+
// Built-in
|
|
636
|
+
if (showBuiltIn) {
|
|
637
|
+
console.log();
|
|
638
|
+
console.log(pc.bold(`Built-in Workflows (${registry.builtIn.length})`));
|
|
639
|
+
if (registry.builtIn.length === 0) {
|
|
640
|
+
console.log(pc.dim(' (none)'));
|
|
641
|
+
} else {
|
|
642
|
+
for (const wf of registry.builtIn) {
|
|
643
|
+
console.log(` ${pc.cyan(wf.name.padEnd(28))} ${wf.description}`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// Official Catalog (@vaicli)
|
|
649
|
+
if (showOfficial) {
|
|
650
|
+
displayPackageList(registry.official, 'Official Catalog (@vaicli)', 'none installed');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Community
|
|
654
|
+
if (showCommunity) {
|
|
655
|
+
displayPackageList(registry.community, 'Community Workflows', 'none installed — install with: vai workflow install <package-name>');
|
|
656
|
+
}
|
|
657
|
+
|
|
284
658
|
console.log();
|
|
285
659
|
console.log(pc.dim('Run with: vai workflow run <name> --input key=value'));
|
|
286
660
|
console.log();
|
|
287
661
|
});
|
|
288
662
|
|
|
663
|
+
// ── workflow install ──
|
|
664
|
+
wfCmd
|
|
665
|
+
.command('install <package>')
|
|
666
|
+
.description('Install a workflow from npm')
|
|
667
|
+
.option('--global', 'Install globally', false)
|
|
668
|
+
.option('--json', 'Output JSON', false)
|
|
669
|
+
.action(async (packageName, opts) => {
|
|
670
|
+
const { installPackage, WORKFLOW_PREFIX, isWorkflowPackage, isOfficialPackage } = require('../lib/npm-utils');
|
|
671
|
+
const { validatePackage, clearRegistryCache } = require('../lib/workflow-registry');
|
|
672
|
+
|
|
673
|
+
// Auto-prefix if needed (but not for scoped packages)
|
|
674
|
+
if (!packageName.startsWith('@') && !packageName.startsWith(WORKFLOW_PREFIX)) {
|
|
675
|
+
packageName = WORKFLOW_PREFIX + packageName;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const telemetry = require('../lib/telemetry');
|
|
679
|
+
console.log(`Installing ${pc.cyan(packageName)}...`);
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
telemetry.send('cli_workflow_install', { packageName });
|
|
683
|
+
const result = installPackage(packageName, { global: opts.global });
|
|
684
|
+
console.log(`${pc.green('✔')} Downloaded ${pc.cyan(packageName)}@${result.version}`);
|
|
685
|
+
|
|
686
|
+
// Validate
|
|
687
|
+
if (result.path) {
|
|
688
|
+
const validation = validatePackage(result.path);
|
|
689
|
+
if (validation.errors.length === 0) {
|
|
690
|
+
const steps = validation.definition?.steps?.length || 0;
|
|
691
|
+
const tools = (validation.pkg?.vai?.tools || []).join(', ');
|
|
692
|
+
console.log(`${pc.green('✔')} Validated workflow definition (${steps} steps${tools ? `, tools: ${tools}` : ''})`);
|
|
693
|
+
|
|
694
|
+
// Display capability flags
|
|
695
|
+
if (validation.definition) {
|
|
696
|
+
const { extractCapabilities } = require('../lib/security-audit');
|
|
697
|
+
const caps = extractCapabilities(validation.definition);
|
|
698
|
+
if (caps.size > 0) {
|
|
699
|
+
const capIcons = { NETWORK: '🌐', WRITE_DB: '💾', LLM: '🤖', LOOP: '🔄', READ_DB: '📊' };
|
|
700
|
+
const capList = [...caps].map(c => `${capIcons[c] || '•'} ${c}`).join(' ');
|
|
701
|
+
console.log(`${pc.dim('Capabilities:')} ${capList}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} else {
|
|
705
|
+
console.log(`${pc.yellow('⚠')} Validation issues:`);
|
|
706
|
+
for (const e of validation.errors) {
|
|
707
|
+
console.log(` ${pc.yellow('-')} ${e}`);
|
|
708
|
+
}
|
|
709
|
+
console.log();
|
|
710
|
+
console.log(pc.dim('The package was installed but the workflow may not execute correctly.'));
|
|
711
|
+
}
|
|
712
|
+
for (const w of validation.warnings) {
|
|
713
|
+
console.log(`${pc.yellow('⚠')} ${w}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
clearRegistryCache();
|
|
718
|
+
|
|
719
|
+
if (opts.json) {
|
|
720
|
+
console.log(JSON.stringify(result, null, 2));
|
|
721
|
+
} else {
|
|
722
|
+
console.log();
|
|
723
|
+
console.log(`Installed. Run with:`);
|
|
724
|
+
console.log(` ${pc.cyan(`vai workflow run ${packageName}`)} --input key=value`);
|
|
725
|
+
}
|
|
726
|
+
} catch (err) {
|
|
727
|
+
console.error(ui.error(err.message));
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// ── workflow uninstall ──
|
|
733
|
+
wfCmd
|
|
734
|
+
.command('uninstall <package>')
|
|
735
|
+
.description('Remove a workflow package')
|
|
736
|
+
.option('--global', 'Uninstall globally', false)
|
|
737
|
+
.action((packageName, opts) => {
|
|
738
|
+
const { uninstallPackage, WORKFLOW_PREFIX } = require('../lib/npm-utils');
|
|
739
|
+
const { clearRegistryCache } = require('../lib/workflow-registry');
|
|
740
|
+
|
|
741
|
+
if (!packageName.startsWith('@') && !packageName.startsWith(WORKFLOW_PREFIX)) {
|
|
742
|
+
packageName = WORKFLOW_PREFIX + packageName;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
console.log(`Uninstalling ${pc.cyan(packageName)}...`);
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
uninstallPackage(packageName, { global: opts.global });
|
|
749
|
+
clearRegistryCache();
|
|
750
|
+
console.log(`${pc.green('✔')} Removed ${packageName}`);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
console.error(ui.error(err.message));
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// ── workflow search ──
|
|
758
|
+
wfCmd
|
|
759
|
+
.command('search <query>')
|
|
760
|
+
.description('Search npm for community workflows')
|
|
761
|
+
.option('--limit <n>', 'Maximum results', '10')
|
|
762
|
+
.option('--json', 'Output JSON', false)
|
|
763
|
+
.action(async (query, opts) => {
|
|
764
|
+
const { searchNpm } = require('../lib/npm-utils');
|
|
765
|
+
|
|
766
|
+
const telemetry = require('../lib/telemetry');
|
|
767
|
+
console.log(`Searching npm for vai-workflow packages matching "${query}"...`);
|
|
768
|
+
console.log();
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const results = await searchNpm(query, { limit: parseInt(opts.limit, 10) });
|
|
772
|
+
telemetry.send('cli_workflow_search', { query: query.slice(0, 50), resultCount: results.length });
|
|
773
|
+
|
|
774
|
+
if (opts.json) {
|
|
775
|
+
console.log(JSON.stringify(results, null, 2));
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (results.length === 0) {
|
|
780
|
+
console.log(pc.dim(' No matching workflow packages found.'));
|
|
781
|
+
console.log();
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
for (const r of results) {
|
|
786
|
+
const badge = r.official ? ` ${pc.green('[OFFICIAL]')}` : '';
|
|
787
|
+
console.log(` ${pc.cyan(r.name)} ${pc.dim(`v${r.version}`)}${badge}`);
|
|
788
|
+
if (r.description) console.log(` ${r.description}`);
|
|
789
|
+
console.log(` ${pc.dim(`by ${r.author}`)}${r.keywords.length ? pc.dim(` | ${r.keywords.slice(0, 5).join(', ')}`) : ''}`);
|
|
790
|
+
console.log();
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
console.log(pc.dim(`Install: vai workflow install <package-name>`));
|
|
794
|
+
console.log();
|
|
795
|
+
} catch (err) {
|
|
796
|
+
console.error(ui.error(err.message));
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// ── workflow info ──
|
|
802
|
+
wfCmd
|
|
803
|
+
.command('info <name>')
|
|
804
|
+
.description('Show detailed info about an installed workflow')
|
|
805
|
+
.option('--json', 'Output JSON', false)
|
|
806
|
+
.action((name, opts) => {
|
|
807
|
+
const { resolveWorkflow, getRegistry } = require('../lib/workflow-registry');
|
|
808
|
+
const { WORKFLOW_PREFIX } = require('../lib/npm-utils');
|
|
809
|
+
|
|
810
|
+
try {
|
|
811
|
+
const resolved = resolveWorkflow(name);
|
|
812
|
+
|
|
813
|
+
if (opts.json) {
|
|
814
|
+
console.log(JSON.stringify({ source: resolved.source, definition: resolved.definition, metadata: resolved.metadata }, null, 2));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const def = resolved.definition;
|
|
819
|
+
console.log();
|
|
820
|
+
|
|
821
|
+
if (resolved.source === 'community' || resolved.source === 'official') {
|
|
822
|
+
const pkg = resolved.metadata?.package || {};
|
|
823
|
+
const author = typeof pkg.author === 'string' ? pkg.author : pkg.author?.name || 'unknown';
|
|
824
|
+
const vai = pkg.vai || {};
|
|
825
|
+
|
|
826
|
+
console.log(`${pc.bold(pc.cyan(pkg.name || name))} ${pc.dim(`v${pkg.version || '?'}`)}`);
|
|
827
|
+
console.log(` ${pkg.description || def.description || ''}`);
|
|
828
|
+
console.log();
|
|
829
|
+
console.log(` ${pc.dim('Author:')} ${author}`);
|
|
830
|
+
console.log(` ${pc.dim('License:')} ${pkg.license || 'unknown'}`);
|
|
831
|
+
console.log(` ${pc.dim('Category:')} ${vai.category || 'utility'}`);
|
|
832
|
+
if (vai.tags?.length) console.log(` ${pc.dim('Tags:')} ${vai.tags.join(', ')}`);
|
|
833
|
+
if (vai.minVaiVersion) console.log(` ${pc.dim('Min vai:')} v${vai.minVaiVersion}`);
|
|
834
|
+
if (vai.tools?.length) console.log(` ${pc.dim('Tools:')} ${vai.tools.join(', ')}`);
|
|
835
|
+
console.log(` ${pc.dim('Steps:')} ${def.steps?.length || 0}`);
|
|
836
|
+
console.log(` ${pc.dim('Source:')} ${resolved.metadata?.path || 'unknown'}`);
|
|
837
|
+
if (pkg.name) console.log(` ${pc.dim('npm:')} https://www.npmjs.com/package/${pkg.name}`);
|
|
838
|
+
} else {
|
|
839
|
+
console.log(`${pc.bold(pc.cyan(def.name || name))} ${pc.dim(`[${resolved.source}]`)}`);
|
|
840
|
+
console.log(` ${def.description || ''}`);
|
|
841
|
+
console.log(` ${pc.dim('Steps:')} ${def.steps?.length || 0}`);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Show inputs
|
|
845
|
+
if (def.inputs && Object.keys(def.inputs).length > 0) {
|
|
846
|
+
console.log();
|
|
847
|
+
console.log(` ${pc.bold('Inputs:')}`);
|
|
848
|
+
for (const [key, schema] of Object.entries(def.inputs)) {
|
|
849
|
+
const req = schema.required ? pc.red('(required)') : pc.dim(`(default: ${schema.default ?? 'none'})`);
|
|
850
|
+
const desc = schema.description || '';
|
|
851
|
+
console.log(` ${pc.cyan(key.padEnd(16))} ${(schema.type || 'string').padEnd(8)} ${req} ${pc.dim(desc)}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
console.log();
|
|
856
|
+
} catch (err) {
|
|
857
|
+
console.error(ui.error(err.message));
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
// ── workflow create ──
|
|
863
|
+
wfCmd
|
|
864
|
+
.command('create')
|
|
865
|
+
.description('Scaffold a publish-ready npm package from a workflow')
|
|
866
|
+
.option('--from <file>', 'Existing workflow JSON to package')
|
|
867
|
+
.option('--name <name>', 'Package name (without vai-workflow- prefix)')
|
|
868
|
+
.option('--author <name>', 'Author name')
|
|
869
|
+
.option('--description <desc>', 'Package description')
|
|
870
|
+
.option('--category <cat>', 'Category (retrieval, analysis, ingestion, domain-specific, utility, integration)')
|
|
871
|
+
.option('--scope <scope>', 'Package scope (e.g. "vaicli" for @vaicli/vai-workflow-*)')
|
|
872
|
+
.option('--output <dir>', 'Output directory')
|
|
873
|
+
.action(async (opts) => {
|
|
874
|
+
const { scaffoldPackage, toPackageName, CATEGORIES, emptyWorkflowTemplate } = require('../lib/workflow-scaffold');
|
|
875
|
+
const { loadWorkflow } = require('../lib/workflow');
|
|
876
|
+
|
|
877
|
+
let definition;
|
|
878
|
+
let name = opts.name;
|
|
879
|
+
let author = opts.author;
|
|
880
|
+
let description = opts.description;
|
|
881
|
+
let category = opts.category;
|
|
882
|
+
|
|
883
|
+
if (opts.from) {
|
|
884
|
+
// Package an existing workflow
|
|
885
|
+
try {
|
|
886
|
+
definition = loadWorkflow(opts.from);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
console.error(ui.error(err.message));
|
|
889
|
+
process.exit(1);
|
|
890
|
+
}
|
|
891
|
+
if (!name) {
|
|
892
|
+
name = definition.name || path.basename(opts.from, '.json').replace('.vai-workflow', '');
|
|
893
|
+
}
|
|
894
|
+
if (!description) {
|
|
895
|
+
description = definition.description;
|
|
896
|
+
}
|
|
897
|
+
} else if (process.stdin.isTTY) {
|
|
898
|
+
// Interactive mode
|
|
899
|
+
try {
|
|
900
|
+
const p = require('@clack/prompts');
|
|
901
|
+
p.intro(pc.bold('Create a new workflow package'));
|
|
902
|
+
|
|
903
|
+
const answers = await p.group({
|
|
904
|
+
name: () => p.text({ message: 'Workflow name', placeholder: 'my-workflow', validate: v => v ? undefined : 'Required' }),
|
|
905
|
+
description: () => p.text({ message: 'Description', placeholder: 'A brief description of what this workflow does' }),
|
|
906
|
+
category: () => p.select({
|
|
907
|
+
message: 'Category',
|
|
908
|
+
options: CATEGORIES.map(c => ({ value: c, label: c })),
|
|
909
|
+
}),
|
|
910
|
+
author: () => p.text({ message: 'Author', placeholder: 'Your Name', defaultValue: getGitAuthor() }),
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
if (p.isCancel(answers)) {
|
|
914
|
+
p.cancel('Cancelled.');
|
|
915
|
+
process.exit(0);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
name = answers.name;
|
|
919
|
+
description = answers.description;
|
|
920
|
+
category = answers.category;
|
|
921
|
+
author = answers.author;
|
|
922
|
+
definition = emptyWorkflowTemplate();
|
|
923
|
+
definition.name = name;
|
|
924
|
+
definition.description = description || '';
|
|
925
|
+
// Add a placeholder step so validation passes
|
|
926
|
+
definition.steps = [{
|
|
927
|
+
id: 'search',
|
|
928
|
+
tool: 'query',
|
|
929
|
+
name: 'Search',
|
|
930
|
+
inputs: { query: '{{ inputs.query }}' },
|
|
931
|
+
}];
|
|
932
|
+
definition.inputs = {
|
|
933
|
+
query: { type: 'string', required: true, description: 'Search query' },
|
|
934
|
+
};
|
|
935
|
+
} catch (err) {
|
|
936
|
+
console.error(ui.error(`Interactive mode failed: ${err.message}`));
|
|
937
|
+
process.exit(1);
|
|
938
|
+
}
|
|
939
|
+
} else {
|
|
940
|
+
console.error(ui.error('Provide --from <file> or run interactively (TTY required).'));
|
|
941
|
+
process.exit(1);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (!name) {
|
|
945
|
+
console.error(ui.error('Workflow name is required. Use --name <name>.'));
|
|
946
|
+
process.exit(1);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
const result = scaffoldPackage({
|
|
951
|
+
definition,
|
|
952
|
+
name,
|
|
953
|
+
author,
|
|
954
|
+
description,
|
|
955
|
+
category,
|
|
956
|
+
scope: opts.scope,
|
|
957
|
+
outputDir: opts.output,
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
const pkgName = toPackageName(name, { scope: opts.scope });
|
|
961
|
+
console.log();
|
|
962
|
+
console.log(`${pc.green('✔')} Created ${pc.cyan(pkgName)}/`);
|
|
963
|
+
for (const f of result.files) {
|
|
964
|
+
console.log(` ${pc.dim('├──')} ${f}`);
|
|
965
|
+
}
|
|
966
|
+
console.log();
|
|
967
|
+
console.log('Next steps:');
|
|
968
|
+
console.log(` 1. ${opts.from ? '' : pc.dim('Edit workflow.json with your workflow definition')}${opts.from ? 'Review README.md' : ''}`);
|
|
969
|
+
console.log(` 2. cd ${pkgName}`);
|
|
970
|
+
console.log(` 3. npm publish`);
|
|
971
|
+
console.log();
|
|
972
|
+
} catch (err) {
|
|
973
|
+
console.error(ui.error(err.message));
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
|
|
289
978
|
// ── workflow init ──
|
|
290
979
|
wfCmd
|
|
291
980
|
.command('init')
|