voyageai-cli 1.30.0 → 1.30.1

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 (55) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +6 -0
  3. package/src/commands/chat.js +32 -11
  4. package/src/commands/export.js +124 -0
  5. package/src/commands/import.js +195 -0
  6. package/src/commands/index-workspace.js +239 -0
  7. package/src/commands/mcp-server.js +113 -3
  8. package/src/commands/playground.js +111 -3
  9. package/src/lib/export/contexts/benchmark-export.js +27 -0
  10. package/src/lib/export/contexts/chat-export.js +41 -0
  11. package/src/lib/export/contexts/explore-export.js +22 -0
  12. package/src/lib/export/contexts/search-export.js +54 -0
  13. package/src/lib/export/contexts/workflow-export.js +80 -0
  14. package/src/lib/export/formats/clipboard-export.js +29 -0
  15. package/src/lib/export/formats/csv-export.js +45 -0
  16. package/src/lib/export/formats/json-export.js +50 -0
  17. package/src/lib/export/formats/markdown-export.js +189 -0
  18. package/src/lib/export/formats/mermaid-export.js +274 -0
  19. package/src/lib/export/formats/pdf-export.js +117 -0
  20. package/src/lib/export/formats/png-export.js +96 -0
  21. package/src/lib/export/formats/svg-export.js +116 -0
  22. package/src/lib/export/index.js +175 -0
  23. package/src/lib/workflow.js +206 -27
  24. package/src/mcp/install.js +280 -7
  25. package/src/mcp/schemas/index.js +40 -0
  26. package/src/mcp/server.js +2 -0
  27. package/src/mcp/tools/workspace.js +463 -0
  28. package/src/playground/announcements.md +52 -5
  29. package/src/playground/index.html +11125 -7796
  30. package/src/playground/vendor/mermaid.min.js +2811 -0
  31. package/src/workflows/rag-chat.json +165 -0
  32. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  33. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  34. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  35. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  36. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  37. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  38. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  39. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  40. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  41. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  42. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  43. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  44. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  45. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  46. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  47. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  48. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  49. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  50. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  51. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
  52. package/src/playground/assets/announcements/appstore.jpg +0 -0
  53. package/src/playground/assets/announcements/circuits.jpg +0 -0
  54. package/src/playground/assets/announcements/csvingest.jpg +0 -0
  55. package/src/playground/assets/announcements/green-wave.jpg +0 -0
@@ -53,21 +53,35 @@ function registerMcpServer(program) {
53
53
  // Subcommand: install
54
54
  cmd
55
55
  .command('install [targets...]')
56
- .description('Install vai MCP server into AI tool configs (claude, claude-code, cursor, windsurf, vscode, or "all")')
56
+ .description('Install vai MCP server into AI tool configs (claude, claude-code, cursor, windsurf, vscode, vscode-insiders, or "all")')
57
57
  .option('--force', 'Overwrite existing vai entry', false)
58
58
  .option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
59
59
  .option('--port <number>', 'HTTP port (http transport only)', (v) => parseInt(v, 10))
60
60
  .option('--api-key <key>', 'Voyage API key to embed in config')
61
+ .option('--mongodb-uri <uri>', 'MongoDB URI to embed in config')
62
+ .option('--db <name>', 'Default database name')
63
+ .option('--collection <name>', 'Default collection name')
64
+ .option('--workspace-path <path>', 'Workspace path for workspace-level config')
65
+ .option('--verbose', 'Enable verbose MCP logging')
66
+ .option('--show-tips', 'Show integration tips after install', true)
61
67
  .action((targets, opts) => {
62
68
  const { TARGETS, installTarget } = require('../mcp/install');
63
69
 
64
70
  if (!targets.length) {
65
71
  console.log('Usage: vai mcp install <target|all>');
66
- console.log(`Available targets: ${Object.keys(TARGETS).join(', ')}, all`);
72
+ console.log(`\nAvailable targets:`);
73
+ for (const [key, target] of Object.entries(TARGETS)) {
74
+ const note = target.requiresWorkspace ? ' (workspace-level)' : '';
75
+ console.log(` ${key.padEnd(18)} ${target.name}${note}`);
76
+ }
77
+ console.log(` ${'all'.padEnd(18)} Install to all global targets`);
67
78
  return;
68
79
  }
69
80
 
70
- const keys = targets.includes('all') ? Object.keys(TARGETS) : targets;
81
+ // Filter out workspace-only targets for 'all'
82
+ const keys = targets.includes('all')
83
+ ? Object.entries(TARGETS).filter(([_, t]) => !t.requiresWorkspace).map(([k]) => k)
84
+ : targets;
71
85
 
72
86
  for (const key of keys) {
73
87
  try {
@@ -76,8 +90,22 @@ function registerMcpServer(program) {
76
90
  transport: opts.transport,
77
91
  port: opts.port,
78
92
  apiKey: opts.apiKey,
93
+ mongodbUri: opts.mongodbUri,
94
+ db: opts.db,
95
+ collection: opts.collection,
96
+ workspacePath: opts.workspacePath,
97
+ verbose: opts.verbose,
79
98
  });
80
99
  console.log(result.installed ? `✅ ${result.message}` : `⚠️ ${result.message}`);
100
+
101
+ // Show tips if available and requested
102
+ if (result.tips && opts.showTips) {
103
+ console.log('');
104
+ for (const tip of result.tips) {
105
+ console.log(` ${tip}`);
106
+ }
107
+ console.log('');
108
+ }
81
109
  } catch (err) {
82
110
  console.error(`❌ ${key}: ${err.message}`);
83
111
  }
@@ -123,6 +151,88 @@ function registerMcpServer(program) {
123
151
  }
124
152
  console.log('');
125
153
  });
154
+
155
+ // Subcommand: diagnose
156
+ cmd
157
+ .command('diagnose [target]')
158
+ .description('Diagnose MCP installation issues for a specific target')
159
+ .action((target) => {
160
+ const { diagnose, TARGETS } = require('../mcp/install');
161
+
162
+ if (!target) {
163
+ console.log('Usage: vai mcp diagnose <target>');
164
+ console.log(`Available targets: ${Object.keys(TARGETS).join(', ')}`);
165
+ return;
166
+ }
167
+
168
+ console.log(`\nvai MCP Diagnostics — ${target}\n`);
169
+
170
+ const results = diagnose(target);
171
+ for (const r of results) {
172
+ const icon = r.level === 'ok' ? '✅' : r.level === 'warning' ? '⚠️ ' : '❌';
173
+ console.log(` ${icon} ${r.message}`);
174
+ }
175
+ console.log('');
176
+ });
177
+
178
+ // Subcommand: sample-config
179
+ cmd
180
+ .command('sample-config <target>')
181
+ .description('Generate sample MCP config for a target (cursor, vscode, etc.)')
182
+ .option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
183
+ .option('--port <number>', 'HTTP port', (v) => parseInt(v, 10))
184
+ .action((target, opts) => {
185
+ const { generateSampleConfig, TARGETS } = require('../mcp/install');
186
+
187
+ if (!TARGETS[target]) {
188
+ console.error(`Unknown target: ${target}. Available: ${Object.keys(TARGETS).join(', ')}`);
189
+ process.exit(1);
190
+ }
191
+
192
+ const sample = generateSampleConfig(target, {
193
+ transport: opts.transport,
194
+ port: opts.port,
195
+ });
196
+
197
+ if (!sample) {
198
+ console.error('Failed to generate sample config');
199
+ process.exit(1);
200
+ }
201
+
202
+ console.log(`\n# Sample MCP config for ${TARGETS[target].name}`);
203
+ console.log(`# Add this to your config file:\n`);
204
+ console.log(sample);
205
+ });
206
+
207
+ // Subcommand: info
208
+ cmd
209
+ .command('info <target>')
210
+ .description('Show detailed information about a target')
211
+ .action((target) => {
212
+ const { getTargetInfo } = require('../mcp/install');
213
+
214
+ const info = getTargetInfo(target);
215
+ if (!info) {
216
+ console.error(`Unknown target: ${target}`);
217
+ process.exit(1);
218
+ }
219
+
220
+ console.log(`\n${info.name}\n`);
221
+ console.log(` Config path: ${info.configPath || 'N/A (workspace-level only)'}`);
222
+ if (info.workspaceConfigPath) {
223
+ console.log(` Workspace path: ${info.workspaceConfigPath}`);
224
+ }
225
+ if (info.requiresWorkspace) {
226
+ console.log(` Note: Requires --workspace-path option`);
227
+ }
228
+ if (info.tips.length > 0) {
229
+ console.log(`\n Tips:`);
230
+ for (const tip of info.tips) {
231
+ console.log(` ${tip}`);
232
+ }
233
+ }
234
+ console.log('');
235
+ });
126
236
  }
127
237
 
128
238
  module.exports = { registerMcpServer };
@@ -247,6 +247,24 @@ function createPlaygroundServer() {
247
247
  return;
248
248
  }
249
249
 
250
+ // Serve vendor assets (bundled JS libraries)
251
+ const vendorMatch = req.url.match(/^\/vendor\/([a-zA-Z0-9_.-]+\.js)$/);
252
+ if (req.method === 'GET' && vendorMatch) {
253
+ const vendorPath = path.join(__dirname, '..', 'playground', 'vendor', vendorMatch[1]);
254
+ if (fs.existsSync(vendorPath)) {
255
+ const data = fs.readFileSync(vendorPath);
256
+ res.writeHead(200, {
257
+ 'Content-Type': 'application/javascript; charset=utf-8',
258
+ 'Cache-Control': 'public, max-age=86400',
259
+ });
260
+ res.end(data);
261
+ } else {
262
+ res.writeHead(404);
263
+ res.end('Vendor asset not found');
264
+ }
265
+ return;
266
+ }
267
+
250
268
  // API: Models
251
269
  if (req.method === 'GET' && req.url === '/api/models') {
252
270
  const models = MODEL_CATALOG.filter(m => !m.legacy && !m.local && !m.unreleased);
@@ -460,6 +478,62 @@ function createPlaygroundServer() {
460
478
  return;
461
479
  }
462
480
 
481
+ // API: MCP status — installation status across all tools
482
+ if (req.method === 'GET' && req.url === '/api/mcp/status') {
483
+ try {
484
+ const { statusAll } = require('../mcp/install');
485
+ const results = statusAll();
486
+ res.writeHead(200, { 'Content-Type': 'application/json' });
487
+ res.end(JSON.stringify(results));
488
+ } catch (err) {
489
+ res.writeHead(500, { 'Content-Type': 'application/json' });
490
+ res.end(JSON.stringify({ error: err.message }));
491
+ }
492
+ return;
493
+ }
494
+
495
+ // API: MCP install — install vai into a target tool
496
+ if (req.method === 'POST' && req.url === '/api/mcp/install') {
497
+ try {
498
+ const body = await readBody(req);
499
+ const { target, force } = JSON.parse(body);
500
+ if (!target) {
501
+ res.writeHead(400, { 'Content-Type': 'application/json' });
502
+ res.end(JSON.stringify({ error: 'target is required' }));
503
+ return;
504
+ }
505
+ const { installTarget } = require('../mcp/install');
506
+ const result = installTarget(target, { force: force || false });
507
+ res.writeHead(200, { 'Content-Type': 'application/json' });
508
+ res.end(JSON.stringify(result));
509
+ } catch (err) {
510
+ res.writeHead(500, { 'Content-Type': 'application/json' });
511
+ res.end(JSON.stringify({ error: err.message }));
512
+ }
513
+ return;
514
+ }
515
+
516
+ // API: MCP uninstall — remove vai from a target tool
517
+ if (req.method === 'POST' && req.url === '/api/mcp/uninstall') {
518
+ try {
519
+ const body = await readBody(req);
520
+ const { target } = JSON.parse(body);
521
+ if (!target) {
522
+ res.writeHead(400, { 'Content-Type': 'application/json' });
523
+ res.end(JSON.stringify({ error: 'target is required' }));
524
+ return;
525
+ }
526
+ const { uninstallTarget } = require('../mcp/install');
527
+ const result = uninstallTarget(target);
528
+ res.writeHead(200, { 'Content-Type': 'application/json' });
529
+ res.end(JSON.stringify(result));
530
+ } catch (err) {
531
+ res.writeHead(500, { 'Content-Type': 'application/json' });
532
+ res.end(JSON.stringify({ error: err.message }));
533
+ }
534
+ return;
535
+ }
536
+
463
537
  // API: Settings origins — where each config value comes from
464
538
  if (req.method === 'GET' && req.url === '/api/settings/origins') {
465
539
  const { resolveLLMConfig } = require('../lib/llm');
@@ -1420,15 +1494,23 @@ function createPlaygroundServer() {
1420
1494
  // API: Validate a workflow definition
1421
1495
  if (req.url === '/api/workflows/validate') {
1422
1496
  const { validateWorkflow } = require('../lib/workflow');
1423
- const { definition } = parsed;
1497
+ const { definition, mode } = parsed;
1424
1498
  if (!definition) {
1425
1499
  res.writeHead(400, { 'Content-Type': 'application/json' });
1426
1500
  res.end(JSON.stringify({ error: 'definition is required' }));
1427
1501
  return;
1428
1502
  }
1429
- const errors = validateWorkflow(definition);
1503
+ const validationMode = mode || 'strict';
1504
+ const result = validateWorkflow(definition, { mode: validationMode });
1505
+
1430
1506
  res.writeHead(200, { 'Content-Type': 'application/json' });
1431
- res.end(JSON.stringify({ valid: errors.length === 0, errors }));
1507
+
1508
+ if (validationMode === 'draft') {
1509
+ res.end(JSON.stringify(result));
1510
+ } else {
1511
+ // Backward compatible format for strict mode
1512
+ res.end(JSON.stringify({ valid: result.length === 0, errors: result }));
1513
+ }
1432
1514
  return;
1433
1515
  }
1434
1516
 
@@ -1532,6 +1614,32 @@ function createPlaygroundServer() {
1532
1614
  }
1533
1615
  }
1534
1616
 
1617
+ // ── Export API endpoints ──
1618
+ const exportMatch = req.url.match(/^\/api\/export\/(workflow|chat|search|benchmark)$/);
1619
+ if (req.method === 'POST' && exportMatch) {
1620
+ const context = exportMatch[1];
1621
+ try {
1622
+ const body = JSON.parse(await readBody(req));
1623
+ const { exportArtifact } = require('../lib/export');
1624
+ const result = await exportArtifact({
1625
+ context,
1626
+ format: body.format || 'json',
1627
+ data: body.data || {},
1628
+ options: body.options || {},
1629
+ });
1630
+ const isBinary = Buffer.isBuffer(result.content);
1631
+ res.writeHead(200, {
1632
+ 'Content-Type': result.mimeType,
1633
+ 'Content-Disposition': `attachment; filename="${result.suggestedFilename}"`,
1634
+ });
1635
+ res.end(isBinary ? result.content : result.content);
1636
+ } catch (err) {
1637
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1638
+ res.end(JSON.stringify({ error: err.message }));
1639
+ }
1640
+ return;
1641
+ }
1642
+
1535
1643
  // 404
1536
1644
  res.writeHead(404, { 'Content-Type': 'application/json' });
1537
1645
  res.end(JSON.stringify({ error: 'Not found' }));
@@ -0,0 +1,27 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalize benchmark data for export.
5
+ * @param {object} data - Raw benchmark data
6
+ * @param {object} options
7
+ * @returns {object} normalized
8
+ */
9
+ function normalizeBenchmark(data, options = {}) {
10
+ const results = data.results || data.rows || [];
11
+ const rows = results.map((r) => {
12
+ const row = { ...r };
13
+ return row;
14
+ });
15
+
16
+ return {
17
+ _context: 'benchmark',
18
+ name: data.name || data.title || 'Benchmark',
19
+ date: data.date || new Date().toISOString(),
20
+ results: rows,
21
+ rows, // alias for CSV renderer
22
+ };
23
+ }
24
+
25
+ const BENCHMARK_FORMATS = ['json', 'csv', 'markdown', 'svg', 'png', 'clipboard'];
26
+
27
+ module.exports = { normalizeBenchmark, BENCHMARK_FORMATS };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalize a chat session for export.
5
+ * @param {object} data - Raw chat session data
6
+ * @param {object} options
7
+ * @returns {object} normalized
8
+ */
9
+ function normalizeChat(data, options = {}) {
10
+ const turns = (data.turns || data.messages || []).map((t) => {
11
+ const turn = {
12
+ role: t.role,
13
+ content: t.content,
14
+ timestamp: t.timestamp,
15
+ };
16
+ if (options.includeSources !== false && t.context) {
17
+ turn.context = t.context;
18
+ }
19
+ if (options.includeMetadata && t.metadata) {
20
+ turn.metadata = t.metadata;
21
+ }
22
+ if (options.includeContextChunks && t.contextChunks) {
23
+ turn.contextChunks = t.contextChunks;
24
+ }
25
+ return turn;
26
+ });
27
+
28
+ return {
29
+ _context: 'chat',
30
+ sessionId: data.sessionId || data.id,
31
+ startedAt: data.startedAt,
32
+ provider: data.provider,
33
+ model: data.model,
34
+ collection: data.collection,
35
+ turns,
36
+ };
37
+ }
38
+
39
+ const CHAT_FORMATS = ['json', 'markdown', 'pdf', 'clipboard'];
40
+
41
+ module.exports = { normalizeChat, CHAT_FORMATS };
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalize explore/visualization data for export.
5
+ * Phase 1 stub — only JSON supported.
6
+ * @param {object} data
7
+ * @param {object} options
8
+ * @returns {object} normalized
9
+ */
10
+ function normalizeExplore(data, options = {}) {
11
+ return {
12
+ _context: 'explore',
13
+ points: data.points || [],
14
+ labels: data.labels || [],
15
+ dimensions: data.dimensions || 2,
16
+ method: data.method || 'pca',
17
+ };
18
+ }
19
+
20
+ const EXPLORE_FORMATS = ['json', 'svg', 'png'];
21
+
22
+ module.exports = { normalizeExplore, EXPLORE_FORMATS };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Normalize search results for export.
5
+ * @param {object} data - Raw search results data
6
+ * @param {object} options
7
+ * @returns {object} normalized
8
+ */
9
+ function normalizeSearch(data, options = {}) {
10
+ const results = (data.results || []).map((r, i) => {
11
+ const item = {
12
+ rank: r.rank || i + 1,
13
+ score: r.score,
14
+ source: r.source || r.path || '',
15
+ };
16
+ if (r.rerankedScore !== undefined) item.rerankedScore = r.rerankedScore;
17
+ if (options.includeFullText) {
18
+ item.text = r.text || '';
19
+ } else {
20
+ item.text = (r.text || '').slice(0, 200);
21
+ }
22
+ if (options.includeMetadata !== false && r.metadata) {
23
+ item.metadata = r.metadata;
24
+ }
25
+ return item;
26
+ });
27
+
28
+ const normalized = {
29
+ _context: 'search',
30
+ results,
31
+ };
32
+
33
+ if (options.includeQuery !== false && data.query) {
34
+ normalized.query = data.query;
35
+ normalized._exportMeta = { query: data.query };
36
+ }
37
+ if (data.collection) normalized.collection = data.collection;
38
+ if (data.model) normalized.model = data.model;
39
+
40
+ // Flat rows for CSV
41
+ normalized.rows = results.map((r) => ({
42
+ rank: r.rank,
43
+ score: r.score,
44
+ reranked_score: r.rerankedScore ?? '',
45
+ source: r.source,
46
+ text_excerpt: (r.text || '').slice(0, 200),
47
+ }));
48
+
49
+ return normalized;
50
+ }
51
+
52
+ const SEARCH_FORMATS = ['json', 'jsonl', 'csv', 'markdown', 'clipboard'];
53
+
54
+ module.exports = { normalizeSearch, SEARCH_FORMATS };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const { buildDependencyGraph } = require('../../workflow');
4
+
5
+ /**
6
+ * Normalize a workflow definition for export.
7
+ * @param {object} workflow - Raw workflow JSON
8
+ * @param {object} options
9
+ * @param {boolean} [options.includeExecution=false]
10
+ * @param {boolean} [options.includeMetadata=false]
11
+ * @returns {object} normalized
12
+ */
13
+ function normalizeWorkflow(workflow, options = {}) {
14
+ const normalized = {
15
+ _context: 'workflow',
16
+ name: workflow.name,
17
+ description: workflow.description,
18
+ version: workflow.version,
19
+ inputs: workflow.inputs || {},
20
+ defaults: workflow.defaults,
21
+ steps: workflow.steps || [],
22
+ output: workflow.output,
23
+ };
24
+
25
+ // Compute dependency map for markdown rendering (Map<string, Set> → plain obj)
26
+ const depGraphMap = buildDependencyGraph(workflow.steps || []);
27
+ const depGraph = {};
28
+ for (const [id, deps] of depGraphMap) {
29
+ depGraph[id] = [...deps];
30
+ }
31
+ normalized._dependencyMap = depGraph;
32
+
33
+ // Count execution layers
34
+ const layerCount = computeLayerCount(workflow.steps || [], depGraph);
35
+ normalized._executionLayers = layerCount;
36
+
37
+ if (options.includeExecution && workflow._execution) {
38
+ normalized._execution = workflow._execution;
39
+ }
40
+
41
+ if (options.includeMetadata) {
42
+ normalized._metadata = {
43
+ _exportedAt: new Date().toISOString(),
44
+ _source: workflow._source || 'local',
45
+ };
46
+ }
47
+
48
+ return normalized;
49
+ }
50
+
51
+ function computeLayerCount(steps, depGraph) {
52
+ if (steps.length === 0) return 0;
53
+ const inDegree = {};
54
+ const ids = steps.map((s) => s.id);
55
+ for (const id of ids) inDegree[id] = (depGraph[id] || []).length;
56
+ const remaining = new Set(ids);
57
+ let layers = 0;
58
+ while (remaining.size > 0) {
59
+ const layer = [];
60
+ for (const id of remaining) {
61
+ if ((inDegree[id] || 0) === 0) layer.push(id);
62
+ }
63
+ if (layer.length === 0) break;
64
+ layers++;
65
+ for (const id of layer) {
66
+ remaining.delete(id);
67
+ for (const [depId, deps] of Object.entries(depGraph)) {
68
+ if (remaining.has(depId) && deps.includes(id)) {
69
+ inDegree[depId]--;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return layers;
75
+ }
76
+
77
+ /** Supported export formats for workflows */
78
+ const WORKFLOW_FORMATS = ['json', 'markdown', 'mermaid', 'svg', 'png', 'clipboard'];
79
+
80
+ module.exports = { normalizeWorkflow, WORKFLOW_FORMATS };
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+ const os = require('os');
5
+
6
+ /**
7
+ * Copy text content to the system clipboard.
8
+ * @param {string} content - Text to copy
9
+ * @returns {boolean} success
10
+ */
11
+ function copyToClipboard(content) {
12
+ const platform = os.platform();
13
+ try {
14
+ if (platform === 'darwin') {
15
+ execSync('pbcopy', { input: content, stdio: ['pipe', 'ignore', 'ignore'] });
16
+ } else if (platform === 'linux') {
17
+ execSync('xclip -selection clipboard', { input: content, stdio: ['pipe', 'ignore', 'ignore'] });
18
+ } else if (platform === 'win32') {
19
+ execSync('clip', { input: content, stdio: ['pipe', 'ignore', 'ignore'] });
20
+ } else {
21
+ return false;
22
+ }
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ module.exports = { copyToClipboard };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Escape a CSV field value.
5
+ * Wraps in quotes if the value contains commas, quotes, or newlines.
6
+ * @param {*} value
7
+ * @returns {string}
8
+ */
9
+ function escapeField(value) {
10
+ if (value === null || value === undefined) return '';
11
+ const str = String(value);
12
+ if (str.includes('"') || str.includes(',') || str.includes('\n') || str.includes('\r')) {
13
+ return '"' + str.replace(/"/g, '""') + '"';
14
+ }
15
+ return str;
16
+ }
17
+
18
+ /**
19
+ * Render an array of objects as CSV.
20
+ * @param {object} normalized - Must have a `rows` or `results` array of flat objects
21
+ * @param {object} options
22
+ * @param {string[]} [options.columns] - Explicit column order; auto-detected if omitted
23
+ * @returns {{ content: string, mimeType: string }}
24
+ */
25
+ function renderCsv(normalized, options = {}) {
26
+ const rows = normalized.rows || normalized.results || [];
27
+ if (!Array.isArray(rows) || rows.length === 0) {
28
+ return { content: '', mimeType: 'text/csv' };
29
+ }
30
+
31
+ // Determine columns
32
+ const columns = options.columns || Object.keys(rows[0]);
33
+
34
+ const header = columns.map(escapeField).join(',');
35
+ const body = rows.map((row) =>
36
+ columns.map((col) => escapeField(row[col])).join(',')
37
+ );
38
+
39
+ return {
40
+ content: [header, ...body].join('\n'),
41
+ mimeType: 'text/csv',
42
+ };
43
+ }
44
+
45
+ module.exports = { renderCsv, escapeField };
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const pkg = require(path.resolve(__dirname, '..', '..', '..', '..', 'package.json'));
5
+
6
+ /**
7
+ * Render normalized data as JSON.
8
+ * @param {object} normalized
9
+ * @param {object} options
10
+ * @returns {{ content: string, mimeType: string }}
11
+ */
12
+ function renderJson(normalized, options = {}) {
13
+ const output = { ...normalized };
14
+ if (options.includeMetadata !== false) {
15
+ output._exportMeta = {
16
+ exportedAt: new Date().toISOString(),
17
+ vaiVersion: pkg.version,
18
+ ...(normalized._exportMeta || {}),
19
+ };
20
+ }
21
+ // Remove internal _exportMeta from source if we rebuilt it
22
+ if (normalized._exportMeta && output._exportMeta !== normalized._exportMeta) {
23
+ delete output._exportMeta;
24
+ }
25
+ return {
26
+ content: JSON.stringify(output, null, 2),
27
+ mimeType: 'application/json',
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Render normalized data as JSONL (one record per line).
33
+ * Expects normalized.results or normalized.items to be an array.
34
+ * @param {object} normalized
35
+ * @param {object} options
36
+ * @returns {{ content: string, mimeType: string }}
37
+ */
38
+ function renderJsonl(normalized, options = {}) {
39
+ const records = normalized.results || normalized.items || [];
40
+ if (!Array.isArray(records)) {
41
+ throw new Error('JSONL export requires an array of records (results or items)');
42
+ }
43
+ const lines = records.map((r) => JSON.stringify(r));
44
+ return {
45
+ content: lines.join('\n'),
46
+ mimeType: 'application/x-ndjson',
47
+ };
48
+ }
49
+
50
+ module.exports = { renderJson, renderJsonl };