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.
Files changed (58) hide show
  1. package/README.md +82 -8
  2. package/package.json +2 -1
  3. package/src/commands/app.js +15 -0
  4. package/src/commands/benchmark.js +22 -8
  5. package/src/commands/chat.js +18 -0
  6. package/src/commands/chunk.js +10 -0
  7. package/src/commands/demo.js +4 -0
  8. package/src/commands/embed.js +13 -0
  9. package/src/commands/estimate.js +3 -0
  10. package/src/commands/eval.js +6 -0
  11. package/src/commands/explain.js +2 -0
  12. package/src/commands/generate.js +2 -0
  13. package/src/commands/ingest.js +4 -0
  14. package/src/commands/init.js +2 -0
  15. package/src/commands/mcp-server.js +2 -0
  16. package/src/commands/models.js +2 -0
  17. package/src/commands/ping.js +7 -0
  18. package/src/commands/pipeline.js +15 -0
  19. package/src/commands/playground.js +685 -8
  20. package/src/commands/query.js +16 -0
  21. package/src/commands/rerank.js +12 -0
  22. package/src/commands/scaffold.js +2 -0
  23. package/src/commands/search.js +11 -0
  24. package/src/commands/similarity.js +9 -0
  25. package/src/commands/store.js +4 -0
  26. package/src/commands/workflow.js +702 -13
  27. package/src/lib/capability-report.js +134 -0
  28. package/src/lib/chat.js +32 -1
  29. package/src/lib/config.js +2 -0
  30. package/src/lib/cost-display.js +107 -0
  31. package/src/lib/explanations.js +94 -0
  32. package/src/lib/llm.js +125 -18
  33. package/src/lib/npm-utils.js +265 -0
  34. package/src/lib/quality-audit.js +71 -0
  35. package/src/lib/security/blocked-domains.json +17 -0
  36. package/src/lib/security-audit.js +198 -0
  37. package/src/lib/telemetry.js +23 -1
  38. package/src/lib/workflow-registry.js +416 -0
  39. package/src/lib/workflow-scaffold.js +380 -0
  40. package/src/lib/workflow-test-runner.js +208 -0
  41. package/src/lib/workflow.js +559 -7
  42. package/src/playground/announcements.md +80 -0
  43. package/src/playground/assets/announcements/appstore.jpg +0 -0
  44. package/src/playground/assets/announcements/circuits.jpg +0 -0
  45. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  46. package/src/playground/assets/announcements/green-wave.jpg +0 -0
  47. package/src/playground/help/workflow-nodes.js +472 -0
  48. package/src/playground/icons/V.png +0 -0
  49. package/src/playground/index.html +3634 -226
  50. package/src/workflows/consistency-check.json +4 -0
  51. package/src/workflows/cost-analysis.json +4 -0
  52. package/src/workflows/enrich-and-ingest.json +56 -0
  53. package/src/workflows/intelligent-ingest.json +66 -0
  54. package/src/workflows/kb-health-report.json +45 -0
  55. package/src/workflows/multi-collection-search.json +4 -0
  56. package/src/workflows/research-and-summarize.json +4 -0
  57. package/src/workflows/search-with-fallback.json +66 -0
  58. package/src/workflows/smart-ingest.json +4 -0
@@ -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 { loadWorkflow, executeWorkflow, buildExecutionPlan, validateWorkflow } = require('../lib/workflow');
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
- definition = loadWorkflow(file);
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 workflow templates')
269
- .action(() => {
270
- const { listBuiltinWorkflows } = require('../lib/workflow');
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
- const workflows = listBuiltinWorkflows();
273
- if (workflows.length === 0) {
274
- console.log(pc.dim('No built-in workflows found.'));
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
- console.log();
279
- console.log(pc.bold('Built-in workflow templates:'));
280
- console.log();
281
- for (const wf of workflows) {
282
- console.log(` ${pc.cyan(wf.name.padEnd(28))} ${wf.description}`);
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')