voyageai-cli 1.30.0 → 1.30.2
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 +4 -4
- package/package.json +1 -1
- package/src/cli.js +8 -0
- package/src/commands/about.js +3 -3
- package/src/commands/chat.js +32 -11
- package/src/commands/code-search.js +751 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/export.js +124 -0
- package/src/commands/import.js +195 -0
- package/src/commands/index-workspace.js +243 -0
- package/src/commands/mcp-server.js +113 -3
- package/src/commands/playground.js +120 -4
- package/src/commands/quickstart.js +4 -4
- package/src/commands/workflow.js +132 -65
- package/src/lib/catalog.js +4 -2
- package/src/lib/code-search.js +315 -0
- package/src/lib/codegen.js +1 -1
- package/src/lib/explanations.js +3 -3
- package/src/lib/export/contexts/benchmark-export.js +27 -0
- package/src/lib/export/contexts/chat-export.js +41 -0
- package/src/lib/export/contexts/explore-export.js +22 -0
- package/src/lib/export/contexts/search-export.js +54 -0
- package/src/lib/export/contexts/workflow-export.js +80 -0
- package/src/lib/export/formats/clipboard-export.js +29 -0
- package/src/lib/export/formats/csv-export.js +45 -0
- package/src/lib/export/formats/json-export.js +50 -0
- package/src/lib/export/formats/markdown-export.js +189 -0
- package/src/lib/export/formats/mermaid-export.js +274 -0
- package/src/lib/export/formats/pdf-export.js +117 -0
- package/src/lib/export/formats/png-export.js +96 -0
- package/src/lib/export/formats/svg-export.js +116 -0
- package/src/lib/export/index.js +175 -0
- package/src/lib/github.js +226 -0
- package/src/lib/template-engine.js +154 -20
- package/src/lib/workflow-builder.js +753 -0
- package/src/lib/workflow-formatters.js +454 -0
- package/src/lib/workflow-input-cache.js +111 -0
- package/src/lib/workflow-scaffold.js +1 -1
- package/src/lib/workflow.js +297 -28
- package/src/mcp/install.js +280 -7
- package/src/mcp/schemas/index.js +170 -0
- package/src/mcp/server.js +19 -4
- package/src/mcp/tools/authoring.js +662 -0
- package/src/mcp/tools/code-search.js +620 -0
- package/src/mcp/tools/ingest.js +2 -5
- package/src/mcp/tools/retrieval.js +2 -15
- package/src/mcp/tools/workspace.js +452 -0
- package/src/mcp/utils.js +20 -0
- package/src/playground/announcements.md +52 -5
- package/src/playground/help/workflow-nodes.js +127 -2
- package/src/playground/index.html +17109 -12438
- package/src/playground/vendor/mermaid.min.js +2811 -0
- package/src/workflows/code-review.json +110 -0
- package/src/workflows/cost-analysis.json +5 -0
- package/src/workflows/rag-chat.json +165 -0
- package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
- package/src/workflows/tests/code-review.happy-path.test.json +121 -0
- package/src/workflows/tests/code-review.no-question.test.json +70 -0
- package/src/workflows/tests/consistency-check.happy-path.test.json +28 -0
- package/src/workflows/tests/consistency-check.missing-source.test.json +26 -0
- package/src/workflows/tests/cost-analysis.happy-path.test.json +28 -0
- package/src/workflows/tests/enrich-and-ingest.happy-path.test.json +38 -0
- package/src/workflows/tests/enrich-and-ingest.notify-fails.test.json +38 -0
- package/src/workflows/tests/intelligent-ingest.all-filtered.test.json +26 -0
- package/src/workflows/tests/intelligent-ingest.happy-path.test.json +28 -0
- package/src/workflows/tests/kb-health-report.custom-queries.test.json +24 -0
- package/src/workflows/tests/kb-health-report.happy-path.test.json +26 -0
- package/src/workflows/tests/multi-collection-search.happy-path.test.json +40 -0
- package/src/workflows/tests/multi-collection-search.one-empty.test.json +28 -0
- package/src/workflows/tests/rag-chat.happy-path.test.json +26 -0
- package/src/workflows/tests/rag-chat.no-relevant-results.test.json +25 -0
- package/src/workflows/tests/research-and-summarize.happy-path.test.json +33 -0
- package/src/workflows/tests/research-and-summarize.no-results.test.json +29 -0
- package/src/workflows/tests/search-with-fallback.empty-both.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.fallback-branch.test.json +24 -0
- package/src/workflows/tests/search-with-fallback.happy-path.test.json +27 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +34 -0
- package/src/workflows/tests/smart-ingest.happy-path.test.json +31 -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
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { loadProject } = require('./project');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CODE_MODEL = 'voyage-code-3';
|
|
8
|
+
const DEFAULT_DB = 'vai_code_search';
|
|
9
|
+
|
|
10
|
+
const CODE_EXTENSIONS = [
|
|
11
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java', '.c', '.cpp',
|
|
12
|
+
'.h', '.hpp', '.cs', '.rb', '.php', '.swift', '.kt', '.scala', '.ex',
|
|
13
|
+
'.exs', '.clj', '.hs', '.ml', '.fs', '.vue', '.svelte', '.sh', '.bash',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const DOC_EXTENSIONS = ['.md', '.rst', '.txt', '.adoc', '.rdoc'];
|
|
17
|
+
|
|
18
|
+
const DEFAULT_IGNORE = [
|
|
19
|
+
'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out', 'target',
|
|
20
|
+
'__pycache__', '.cache', '.next', '.nuxt', 'coverage', '.nyc_output',
|
|
21
|
+
'vendor', 'venv', '.venv', 'env', '.idea', '.vscode',
|
|
22
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock',
|
|
23
|
+
'*.min.js', '*.min.css', '*.map', '*.chunk.js',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Language-aware function/class boundary patterns.
|
|
28
|
+
*/
|
|
29
|
+
const BOUNDARY_PATTERNS = {
|
|
30
|
+
js: /^(?:(?:export\s+)?(?:async\s+)?function\s+\w+|(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:function|\()|(?:export\s+)?class\s+\w+|module\.exports)/m,
|
|
31
|
+
ts: /^(?:(?:export\s+)?(?:async\s+)?function\s+\w+|(?:export\s+)?(?:const|let)\s+\w+\s*[=:]|(?:export\s+)?(?:class|interface|type|enum)\s+\w+)/m,
|
|
32
|
+
py: /^(?:def\s+|async\s+def\s+|class\s+)/m,
|
|
33
|
+
go: /^(?:func\s+|type\s+\w+\s+(?:struct|interface))/m,
|
|
34
|
+
rs: /^(?:(?:pub\s+)?fn\s+|(?:pub\s+)?(?:struct|enum|trait|impl)\s+)/m,
|
|
35
|
+
java: /^(?:\s*(?:public|private|protected)\s+(?:static\s+)?(?:class|interface|void|\w+)\s+\w+)/m,
|
|
36
|
+
rb: /^(?:def\s+|class\s+|module\s+)/m,
|
|
37
|
+
php: /^(?:\s*(?:public|private|protected)?\s*(?:static\s+)?function\s+|class\s+)/m,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the boundary pattern for a file extension.
|
|
42
|
+
* @param {string} ext
|
|
43
|
+
* @returns {RegExp|null}
|
|
44
|
+
*/
|
|
45
|
+
function getBoundaryPattern(ext) {
|
|
46
|
+
const lang = ext.replace('.', '');
|
|
47
|
+
const map = {
|
|
48
|
+
js: 'js', jsx: 'js', mjs: 'js', cjs: 'js',
|
|
49
|
+
ts: 'ts', tsx: 'ts', mts: 'ts',
|
|
50
|
+
py: 'py',
|
|
51
|
+
go: 'go',
|
|
52
|
+
rs: 'rs',
|
|
53
|
+
java: 'java', kt: 'java', scala: 'java',
|
|
54
|
+
rb: 'rb',
|
|
55
|
+
php: 'php',
|
|
56
|
+
};
|
|
57
|
+
const key = map[lang];
|
|
58
|
+
return key ? BOUNDARY_PATTERNS[key] : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Smart chunk code: try splitting by function/class boundaries first,
|
|
63
|
+
* fall back to recursive character-based chunking.
|
|
64
|
+
* @param {string} content
|
|
65
|
+
* @param {string} filePath
|
|
66
|
+
* @param {object} opts
|
|
67
|
+
* @returns {Array<{text: string, startLine: number, endLine: number, type: string}>}
|
|
68
|
+
*/
|
|
69
|
+
function smartChunkCode(content, filePath, opts = {}) {
|
|
70
|
+
const { chunk } = require('./chunker');
|
|
71
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
72
|
+
const pattern = getBoundaryPattern(ext);
|
|
73
|
+
const chunkSize = opts.chunkSize || 512;
|
|
74
|
+
const chunkOverlap = opts.chunkOverlap || 50;
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
|
|
77
|
+
// Try boundary-based splitting
|
|
78
|
+
if (pattern) {
|
|
79
|
+
const boundaries = [];
|
|
80
|
+
for (let i = 0; i < lines.length; i++) {
|
|
81
|
+
if (pattern.test(lines[i])) {
|
|
82
|
+
boundaries.push(i);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (boundaries.length > 1) {
|
|
87
|
+
const chunks = [];
|
|
88
|
+
for (let i = 0; i < boundaries.length; i++) {
|
|
89
|
+
const start = boundaries[i];
|
|
90
|
+
const end = i + 1 < boundaries.length ? boundaries[i + 1] : lines.length;
|
|
91
|
+
const text = lines.slice(start, end).join('\n').trim();
|
|
92
|
+
if (text.length >= 20) {
|
|
93
|
+
if (text.length > chunkSize * 2) {
|
|
94
|
+
const subChunks = chunk(text, { strategy: 'recursive', size: chunkSize, overlap: chunkOverlap });
|
|
95
|
+
let lineOffset = start;
|
|
96
|
+
for (const sc of subChunks) {
|
|
97
|
+
const scLines = sc.split('\n').length;
|
|
98
|
+
chunks.push({ text: sc, startLine: lineOffset + 1, endLine: lineOffset + scLines, type: 'boundary' });
|
|
99
|
+
lineOffset += scLines;
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
chunks.push({ text, startLine: start + 1, endLine: end, type: 'boundary' });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (boundaries[0] > 0) {
|
|
107
|
+
const preamble = lines.slice(0, boundaries[0]).join('\n').trim();
|
|
108
|
+
if (preamble.length >= 20) {
|
|
109
|
+
chunks.unshift({ text: preamble, startLine: 1, endLine: boundaries[0], type: 'preamble' });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (chunks.length > 0) return chunks;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fallback: recursive chunking with line number tracking
|
|
117
|
+
const { chunk: chunkFn } = require('./chunker');
|
|
118
|
+
const textChunks = chunkFn(content, { strategy: 'recursive', size: chunkSize, overlap: chunkOverlap });
|
|
119
|
+
const result = [];
|
|
120
|
+
let searchFrom = 0;
|
|
121
|
+
for (const tc of textChunks) {
|
|
122
|
+
const firstLine = tc.split('\n')[0];
|
|
123
|
+
let startLine = searchFrom;
|
|
124
|
+
for (let i = searchFrom; i < lines.length; i++) {
|
|
125
|
+
if (lines[i].includes(firstLine.trim().slice(0, 40))) {
|
|
126
|
+
startLine = i;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const chunkLines = tc.split('\n').length;
|
|
131
|
+
result.push({ text: tc, startLine: startLine + 1, endLine: startLine + chunkLines, type: 'character' });
|
|
132
|
+
searchFrom = startLine + 1;
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Extract symbol names from code.
|
|
139
|
+
* @param {string} content
|
|
140
|
+
* @param {string} filePath
|
|
141
|
+
* @returns {string[]}
|
|
142
|
+
*/
|
|
143
|
+
function extractSymbols(content, filePath) {
|
|
144
|
+
const ext = path.extname(filePath).toLowerCase().slice(1);
|
|
145
|
+
const patterns = {
|
|
146
|
+
js: [/(?:function\s+|const\s+|let\s+|var\s+)(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\(|=>)|\()/g, /class\s+(\w+)/g],
|
|
147
|
+
ts: [/(?:function\s+|const\s+|let\s+)(\w+)\s*(?:=\s*(?:async\s+)?(?:function|\(|=>)|[<(])/g, /(?:class|interface|type)\s+(\w+)/g],
|
|
148
|
+
py: [/(?:def|async def)\s+(\w+)\s*\(/g, /class\s+(\w+)/g],
|
|
149
|
+
go: [/func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/g, /type\s+(\w+)\s+struct/g],
|
|
150
|
+
rs: [/fn\s+(\w+)\s*[<(]/g, /(?:struct|enum|trait)\s+(\w+)/g],
|
|
151
|
+
java: [/(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(/g, /class\s+(\w+)/g],
|
|
152
|
+
rb: [/def\s+(\w+)/g, /class\s+(\w+)/g],
|
|
153
|
+
php: [/function\s+(\w+)/g, /class\s+(\w+)/g],
|
|
154
|
+
};
|
|
155
|
+
const langMap = { jsx: 'js', mjs: 'js', cjs: 'js', tsx: 'ts', mts: 'ts', kt: 'java', scala: 'java' };
|
|
156
|
+
const lang = langMap[ext] || ext;
|
|
157
|
+
const langPatterns = patterns[lang] || patterns.js;
|
|
158
|
+
const symbols = [];
|
|
159
|
+
for (const p of langPatterns) {
|
|
160
|
+
let m;
|
|
161
|
+
while ((m = p.exec(content)) !== null) {
|
|
162
|
+
if (m[1] && !symbols.includes(m[1])) symbols.push(m[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return symbols.slice(0, 50);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Parse .gitignore patterns from a directory.
|
|
170
|
+
* @param {string} dirPath
|
|
171
|
+
* @returns {string[]}
|
|
172
|
+
*/
|
|
173
|
+
function loadGitignore(dirPath) {
|
|
174
|
+
const gitignorePath = path.join(dirPath, '.gitignore');
|
|
175
|
+
try {
|
|
176
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
177
|
+
return content
|
|
178
|
+
.split('\n')
|
|
179
|
+
.map(l => l.trim())
|
|
180
|
+
.filter(l => l && !l.startsWith('#'));
|
|
181
|
+
} catch {
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if a path should be ignored.
|
|
188
|
+
* @param {string} filePath
|
|
189
|
+
* @param {string[]} patterns
|
|
190
|
+
* @returns {boolean}
|
|
191
|
+
*/
|
|
192
|
+
function shouldIgnore(filePath, patterns) {
|
|
193
|
+
const basename = path.basename(filePath);
|
|
194
|
+
for (const pattern of patterns) {
|
|
195
|
+
if (pattern.startsWith('*')) {
|
|
196
|
+
if (basename.endsWith(pattern.slice(1))) return true;
|
|
197
|
+
} else if (filePath.includes(pattern) || basename === pattern) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Recursively find code files respecting .gitignore.
|
|
206
|
+
* @param {string} dirPath
|
|
207
|
+
* @param {object} opts
|
|
208
|
+
* @returns {Promise<string[]>}
|
|
209
|
+
*/
|
|
210
|
+
async function findCodeFiles(dirPath, opts = {}) {
|
|
211
|
+
const maxFiles = opts.maxFiles || 5000;
|
|
212
|
+
const maxFileSize = opts.maxFileSize || 100000;
|
|
213
|
+
const gitignorePatterns = loadGitignore(dirPath);
|
|
214
|
+
const allPatterns = [...DEFAULT_IGNORE, ...gitignorePatterns];
|
|
215
|
+
const files = [];
|
|
216
|
+
|
|
217
|
+
async function walk(dir) {
|
|
218
|
+
if (files.length >= maxFiles) return;
|
|
219
|
+
let entries;
|
|
220
|
+
try {
|
|
221
|
+
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
222
|
+
} catch { return; }
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (files.length >= maxFiles) break;
|
|
225
|
+
const fullPath = path.join(dir, entry.name);
|
|
226
|
+
if (shouldIgnore(fullPath, allPatterns)) continue;
|
|
227
|
+
if (entry.isDirectory()) {
|
|
228
|
+
await walk(fullPath);
|
|
229
|
+
} else if (entry.isFile()) {
|
|
230
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
231
|
+
if (!CODE_EXTENSIONS.includes(ext)) continue;
|
|
232
|
+
try {
|
|
233
|
+
const stats = await fs.promises.stat(fullPath);
|
|
234
|
+
if (stats.size > maxFileSize || stats.size === 0) continue;
|
|
235
|
+
} catch { continue; }
|
|
236
|
+
files.push(fullPath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
await walk(dirPath);
|
|
242
|
+
return files;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Derive a collection name from a directory path.
|
|
247
|
+
* @param {string} dirPath
|
|
248
|
+
* @returns {string}
|
|
249
|
+
*/
|
|
250
|
+
function deriveCollectionName(dirPath) {
|
|
251
|
+
try {
|
|
252
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dirPath, 'package.json'), 'utf-8'));
|
|
253
|
+
if (pkg.name) return pkg.name.replace(/[^a-zA-Z0-9_-]/g, '_') + '_code';
|
|
254
|
+
} catch { /* ignore */ }
|
|
255
|
+
return path.basename(path.resolve(dirPath)).replace(/[^a-zA-Z0-9_-]/g, '_') + '_code';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolve db/collection from options, .vai.json codeSearch config, or defaults.
|
|
260
|
+
* @param {object} opts
|
|
261
|
+
* @param {string} [workspacePath]
|
|
262
|
+
* @returns {{db: string, collection: string, model: string, projectConfig: object}}
|
|
263
|
+
*/
|
|
264
|
+
function resolveConfig(opts, workspacePath) {
|
|
265
|
+
const { config: proj } = loadProject(workspacePath);
|
|
266
|
+
const cs = proj.codeSearch || {};
|
|
267
|
+
const db = opts.db || cs.db || proj.db || DEFAULT_DB;
|
|
268
|
+
const collection = opts.collection || cs.collection || deriveCollectionName(workspacePath || process.cwd());
|
|
269
|
+
const model = opts.model || cs.model || DEFAULT_CODE_MODEL;
|
|
270
|
+
return { db, collection, model, projectConfig: proj };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Auto-select the best embedding model based on file types.
|
|
275
|
+
* @param {string[]} files - Array of file paths
|
|
276
|
+
* @param {object} [projectConfig] - Project config from .vai.json
|
|
277
|
+
* @returns {string}
|
|
278
|
+
*/
|
|
279
|
+
function selectCodeModel(files, projectConfig) {
|
|
280
|
+
// User override always wins
|
|
281
|
+
if (projectConfig?.codeSearch?.model) {
|
|
282
|
+
return projectConfig.codeSearch.model;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const total = files.length;
|
|
286
|
+
if (total === 0) return DEFAULT_CODE_MODEL;
|
|
287
|
+
|
|
288
|
+
const codeFiles = files.filter(f => CODE_EXTENSIONS.includes(path.extname(f).toLowerCase()));
|
|
289
|
+
const docFiles = files.filter(f => DOC_EXTENSIONS.includes(path.extname(f).toLowerCase()));
|
|
290
|
+
|
|
291
|
+
const codeRatio = codeFiles.length / total;
|
|
292
|
+
const docRatio = docFiles.length / total;
|
|
293
|
+
|
|
294
|
+
if (codeRatio >= 0.7) return 'voyage-code-3';
|
|
295
|
+
if (docRatio >= 0.7) return 'voyage-4-large';
|
|
296
|
+
return 'voyage-code-3';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
DEFAULT_CODE_MODEL,
|
|
301
|
+
DEFAULT_DB,
|
|
302
|
+
CODE_EXTENSIONS,
|
|
303
|
+
DOC_EXTENSIONS,
|
|
304
|
+
DEFAULT_IGNORE,
|
|
305
|
+
BOUNDARY_PATTERNS,
|
|
306
|
+
getBoundaryPattern,
|
|
307
|
+
smartChunkCode,
|
|
308
|
+
extractSymbols,
|
|
309
|
+
loadGitignore,
|
|
310
|
+
shouldIgnore,
|
|
311
|
+
findCodeFiles,
|
|
312
|
+
deriveCollectionName,
|
|
313
|
+
resolveConfig,
|
|
314
|
+
selectCodeModel,
|
|
315
|
+
};
|
package/src/lib/codegen.js
CHANGED
|
@@ -302,7 +302,7 @@ function renderTemplate(target, name, context) {
|
|
|
302
302
|
function buildContext(project, options = {}) {
|
|
303
303
|
const context = {
|
|
304
304
|
// Core config
|
|
305
|
-
model: options.model || project.model || 'voyage-
|
|
305
|
+
model: options.model || project.model || 'voyage-4-large',
|
|
306
306
|
db: options.db || project.db || 'myapp',
|
|
307
307
|
collection: options.collection || project.collection || 'documents',
|
|
308
308
|
field: options.field || project.field || 'embedding',
|
package/src/lib/explanations.js
CHANGED
|
@@ -549,7 +549,7 @@ const concepts = {
|
|
|
549
549
|
``,
|
|
550
550
|
`${pc.bold('In practice:')} You don't need to do anything special to use MoE — the API`,
|
|
551
551
|
`interface is identical. The architecture difference shows up in quality and cost:`,
|
|
552
|
-
` ${pc.dim('•')} voyage-4-large: $0.12/1M tokens
|
|
552
|
+
` ${pc.dim('•')} voyage-4-large: $0.12/1M tokens, best quality via MoE architecture`,
|
|
553
553
|
` ${pc.dim('•')} 40% cheaper than comparable dense models at the same quality tier`,
|
|
554
554
|
].join('\n'),
|
|
555
555
|
links: [
|
|
@@ -616,9 +616,9 @@ const concepts = {
|
|
|
616
616
|
``,
|
|
617
617
|
`${pc.bold('Current standings (Jan 2026):')}`,
|
|
618
618
|
` ${pc.cyan('voyage-4-large')} ${pc.bold('71.41')} ${pc.dim('— SOTA, MoE architecture')}`,
|
|
619
|
-
` ${pc.cyan('voyage-4')} ${pc.bold('70.07')} ${pc.dim('—
|
|
619
|
+
` ${pc.cyan('voyage-4')} ${pc.bold('70.07')} ${pc.dim('— balanced quality/cost')}`,
|
|
620
620
|
` ${pc.cyan('Gemini Embedding 001')} ${pc.bold('68.66')} ${pc.dim('— Google')}`,
|
|
621
|
-
` ${pc.cyan('voyage-4-lite')} ${pc.bold('68.10')} ${pc.dim('—
|
|
621
|
+
` ${pc.cyan('voyage-4-lite')} ${pc.bold('68.10')} ${pc.dim('— best budget option')}`,
|
|
622
622
|
` ${pc.cyan('Cohere Embed v4')} ${pc.bold('65.75')} ${pc.dim('— Cohere')}`,
|
|
623
623
|
` ${pc.cyan('OpenAI v3 Large')} ${pc.bold('62.57')} ${pc.dim('— OpenAI')}`,
|
|
624
624
|
``,
|
|
@@ -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 };
|