voyageai-cli 1.29.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 (85) hide show
  1. package/README.md +82 -8
  2. package/package.json +1 -1
  3. package/src/cli.js +6 -0
  4. package/src/commands/benchmark.js +22 -8
  5. package/src/commands/chat.js +50 -11
  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/export.js +124 -0
  13. package/src/commands/generate.js +2 -0
  14. package/src/commands/import.js +195 -0
  15. package/src/commands/index-workspace.js +239 -0
  16. package/src/commands/ingest.js +4 -0
  17. package/src/commands/init.js +2 -0
  18. package/src/commands/mcp-server.js +115 -3
  19. package/src/commands/models.js +2 -0
  20. package/src/commands/ping.js +7 -0
  21. package/src/commands/pipeline.js +15 -0
  22. package/src/commands/playground.js +163 -9
  23. package/src/commands/query.js +16 -0
  24. package/src/commands/rerank.js +12 -0
  25. package/src/commands/scaffold.js +2 -0
  26. package/src/commands/search.js +11 -0
  27. package/src/commands/similarity.js +9 -0
  28. package/src/commands/store.js +4 -0
  29. package/src/commands/workflow.js +286 -0
  30. package/src/lib/capability-report.js +134 -0
  31. package/src/lib/chat.js +32 -1
  32. package/src/lib/config.js +2 -0
  33. package/src/lib/cost-display.js +107 -0
  34. package/src/lib/explanations.js +6 -0
  35. package/src/lib/export/contexts/benchmark-export.js +27 -0
  36. package/src/lib/export/contexts/chat-export.js +41 -0
  37. package/src/lib/export/contexts/explore-export.js +22 -0
  38. package/src/lib/export/contexts/search-export.js +54 -0
  39. package/src/lib/export/contexts/workflow-export.js +80 -0
  40. package/src/lib/export/formats/clipboard-export.js +29 -0
  41. package/src/lib/export/formats/csv-export.js +45 -0
  42. package/src/lib/export/formats/json-export.js +50 -0
  43. package/src/lib/export/formats/markdown-export.js +189 -0
  44. package/src/lib/export/formats/mermaid-export.js +274 -0
  45. package/src/lib/export/formats/pdf-export.js +117 -0
  46. package/src/lib/export/formats/png-export.js +96 -0
  47. package/src/lib/export/formats/svg-export.js +116 -0
  48. package/src/lib/export/index.js +175 -0
  49. package/src/lib/llm.js +125 -18
  50. package/src/lib/quality-audit.js +71 -0
  51. package/src/lib/security/blocked-domains.json +17 -0
  52. package/src/lib/security-audit.js +198 -0
  53. package/src/lib/telemetry.js +23 -1
  54. package/src/lib/workflow-scaffold.js +61 -0
  55. package/src/lib/workflow-test-runner.js +208 -0
  56. package/src/lib/workflow.js +333 -28
  57. package/src/mcp/install.js +280 -7
  58. package/src/mcp/schemas/index.js +40 -0
  59. package/src/mcp/server.js +2 -0
  60. package/src/mcp/tools/workspace.js +463 -0
  61. package/src/playground/announcements.md +56 -0
  62. package/src/playground/help/workflow-nodes.js +472 -0
  63. package/src/playground/index.html +13134 -8507
  64. package/src/playground/vendor/mermaid.min.js +2811 -0
  65. package/src/workflows/rag-chat.json +165 -0
  66. package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
  67. package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
  68. package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
  69. package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
  70. package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
  71. package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
  72. package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
  73. package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
  74. package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
  75. package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
  76. package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
  77. package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
  78. package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
  79. package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
  80. package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
  81. package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
  82. package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
  83. package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
  84. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
  85. package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -0
@@ -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 };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Render normalized data as Markdown.
5
+ * The shape of `normalized` varies by context — the context module sets a `_context` key.
6
+ * @param {object} normalized
7
+ * @param {object} options
8
+ * @returns {{ content: string, mimeType: string }}
9
+ */
10
+ function renderMarkdown(normalized, options = {}) {
11
+ const ctx = normalized._context;
12
+ let md;
13
+ switch (ctx) {
14
+ case 'workflow':
15
+ md = renderWorkflowMd(normalized, options);
16
+ break;
17
+ case 'search':
18
+ md = renderSearchMd(normalized, options);
19
+ break;
20
+ case 'chat':
21
+ md = renderChatMd(normalized, options);
22
+ break;
23
+ case 'benchmark':
24
+ md = renderBenchmarkMd(normalized, options);
25
+ break;
26
+ default:
27
+ md = '# Export\n\n```json\n' + JSON.stringify(normalized, null, 2) + '\n```\n';
28
+ }
29
+ return { content: md, mimeType: 'text/markdown' };
30
+ }
31
+
32
+ // ── Workflow ──
33
+
34
+ function renderWorkflowMd(n, opts) {
35
+ const lines = [];
36
+ lines.push(`# ${n.name || 'Workflow'}`);
37
+ lines.push('');
38
+ if (n.description) {
39
+ lines.push(n.description);
40
+ lines.push('');
41
+ }
42
+
43
+ // Metadata
44
+ if (n.version) lines.push(`**Version:** ${n.version}`);
45
+ const tools = (n.steps || []).map((s) => s.tool).filter(Boolean);
46
+ const unique = [...new Set(tools)];
47
+ if (unique.length) lines.push(`**Tools used:** ${unique.join(', ')}`);
48
+ const layers = n._executionLayers || null;
49
+ if (layers) lines.push(`**Execution layers:** ${layers} (sequential)`);
50
+ lines.push('');
51
+
52
+ // Inputs table
53
+ if (n.inputs && Object.keys(n.inputs).length > 0) {
54
+ lines.push('## Inputs');
55
+ lines.push('');
56
+ lines.push('| Parameter | Type | Required | Default | Description |');
57
+ lines.push('|-----------|------|----------|---------|-------------|');
58
+ for (const [key, schema] of Object.entries(n.inputs)) {
59
+ const type = schema.type || 'string';
60
+ const req = schema.required ? 'Yes' : 'No';
61
+ const def = schema.default !== undefined ? String(schema.default) : '—';
62
+ const desc = schema.description || '';
63
+ lines.push(`| ${key} | ${type} | ${req} | ${def} | ${desc} |`);
64
+ }
65
+ lines.push('');
66
+ }
67
+
68
+ // Steps
69
+ if (n.steps && n.steps.length > 0) {
70
+ lines.push('## Steps');
71
+ lines.push('');
72
+ const deps = n._dependencyMap || {};
73
+ n.steps.forEach((step, i) => {
74
+ const label = step.name || step.description || step.id;
75
+ lines.push(`### ${i + 1}. ${label}`);
76
+ if (step.tool) lines.push(`- **Tool:** ${step.tool}`);
77
+ const stepDeps = deps[step.id];
78
+ if (stepDeps && stepDeps.length > 0) {
79
+ lines.push(`- **Depends on:** ${stepDeps.join(', ')}`);
80
+ } else {
81
+ lines.push('- **Depends on:** (none — first step)');
82
+ }
83
+ if (step.description && step.description !== label) {
84
+ lines.push(`- **Configuration:** ${step.description}`);
85
+ }
86
+ lines.push('');
87
+ });
88
+ }
89
+
90
+ // Output
91
+ if (n.output) {
92
+ lines.push('## Output');
93
+ lines.push('');
94
+ lines.push(`Returns: \`${n.output}\``);
95
+ lines.push('');
96
+ }
97
+
98
+ return lines.join('\n');
99
+ }
100
+
101
+ // ── Search Results ──
102
+
103
+ function renderSearchMd(n, opts) {
104
+ const lines = [];
105
+ lines.push('# Search Results');
106
+ lines.push('');
107
+ if (n.query) lines.push(`**Query:** ${n.query}`);
108
+ if (n.collection) lines.push(`**Collection:** ${n.collection}`);
109
+ if (n.model) lines.push(`**Model:** ${n.model}`);
110
+ const total = (n.results || []).length;
111
+ lines.push(`**Results:** ${total}`);
112
+ lines.push('');
113
+
114
+ (n.results || []).forEach((r, i) => {
115
+ lines.push(`### ${i + 1}. ${r.source || 'Result'} (score: ${r.score ?? '—'})`);
116
+ if (r.rerankedScore !== undefined) lines.push(`- **Reranked score:** ${r.rerankedScore}`);
117
+ const text = opts.includeFullText ? r.text : (r.text || '').slice(0, 200);
118
+ if (text) {
119
+ lines.push('');
120
+ lines.push(`> ${text}`);
121
+ }
122
+ lines.push('');
123
+ });
124
+
125
+ return lines.join('\n');
126
+ }
127
+
128
+ // ── Chat Session ──
129
+
130
+ function renderChatMd(n, opts) {
131
+ const lines = [];
132
+ const sessionId = n.sessionId || n.id || 'unknown';
133
+ lines.push(`# Chat Session: ${sessionId}`);
134
+ lines.push('');
135
+ if (n.startedAt) lines.push(`**Date:** ${n.startedAt}`);
136
+ if (n.provider && n.model) lines.push(`**Provider:** ${n.provider} (${n.model})`);
137
+ else if (n.model) lines.push(`**Model:** ${n.model}`);
138
+ if (n.collection) lines.push(`**Knowledge Base:** ${n.collection}`);
139
+ const turns = (n.turns || []).length;
140
+ lines.push(`**Turns:** ${turns}`);
141
+ lines.push('');
142
+ lines.push('---');
143
+ lines.push('');
144
+
145
+ (n.turns || []).forEach((turn) => {
146
+ const role = turn.role === 'user' ? 'User' : 'vai';
147
+ lines.push(`**${role}:**`);
148
+ lines.push(turn.content || '');
149
+ if (opts.includeSources !== false && turn.context && turn.context.length > 0) {
150
+ lines.push('');
151
+ lines.push('> **Sources:**');
152
+ turn.context.forEach((src, i) => {
153
+ const rel = src.score !== undefined ? ` (relevance: ${src.score})` : '';
154
+ lines.push(`> ${i + 1}. ${src.source || 'unknown'}${rel}`);
155
+ });
156
+ }
157
+ lines.push('');
158
+ lines.push('---');
159
+ lines.push('');
160
+ });
161
+
162
+ return lines.join('\n');
163
+ }
164
+
165
+ // ── Benchmark ──
166
+
167
+ function renderBenchmarkMd(n, opts) {
168
+ const lines = [];
169
+ lines.push('# Benchmark Results');
170
+ lines.push('');
171
+ if (n.name) lines.push(`**Benchmark:** ${n.name}`);
172
+ if (n.date) lines.push(`**Date:** ${n.date}`);
173
+ lines.push('');
174
+
175
+ const results = n.results || n.rows || [];
176
+ if (results.length > 0) {
177
+ const cols = Object.keys(results[0]);
178
+ lines.push('| ' + cols.join(' | ') + ' |');
179
+ lines.push('| ' + cols.map(() => '---').join(' | ') + ' |');
180
+ results.forEach((row) => {
181
+ lines.push('| ' + cols.map((c) => String(row[c] ?? '')).join(' | ') + ' |');
182
+ });
183
+ lines.push('');
184
+ }
185
+
186
+ return lines.join('\n');
187
+ }
188
+
189
+ module.exports = { renderMarkdown };