voyageai-cli 1.30.1 → 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.
Files changed (37) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +2 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/code-search.js +751 -0
  6. package/src/commands/doctor.js +1 -1
  7. package/src/commands/index-workspace.js +9 -5
  8. package/src/commands/playground.js +9 -1
  9. package/src/commands/quickstart.js +4 -4
  10. package/src/commands/workflow.js +132 -65
  11. package/src/lib/catalog.js +4 -2
  12. package/src/lib/code-search.js +315 -0
  13. package/src/lib/codegen.js +1 -1
  14. package/src/lib/explanations.js +3 -3
  15. package/src/lib/github.js +226 -0
  16. package/src/lib/template-engine.js +154 -20
  17. package/src/lib/workflow-builder.js +753 -0
  18. package/src/lib/workflow-formatters.js +454 -0
  19. package/src/lib/workflow-input-cache.js +111 -0
  20. package/src/lib/workflow-scaffold.js +1 -1
  21. package/src/lib/workflow.js +91 -1
  22. package/src/mcp/schemas/index.js +130 -0
  23. package/src/mcp/server.js +17 -4
  24. package/src/mcp/tools/authoring.js +662 -0
  25. package/src/mcp/tools/code-search.js +620 -0
  26. package/src/mcp/tools/ingest.js +2 -5
  27. package/src/mcp/tools/retrieval.js +2 -15
  28. package/src/mcp/tools/workspace.js +1 -12
  29. package/src/mcp/utils.js +20 -0
  30. package/src/playground/help/workflow-nodes.js +127 -2
  31. package/src/playground/index.html +1366 -24
  32. package/src/workflows/code-review.json +110 -0
  33. package/src/workflows/cost-analysis.json +5 -0
  34. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  35. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  36. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  37. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
@@ -0,0 +1,454 @@
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+ const { formatTable } = require('./format');
5
+
6
+ /**
7
+ * Valid output format names (excluding 'value:<path>' which is handled separately).
8
+ */
9
+ const FORMAT_TYPES = new Set(['json', 'table', 'markdown', 'text', 'csv']);
10
+
11
+ // ════════════════════════════════════════════════════════════════════
12
+ // Shape Detection
13
+ // ════════════════════════════════════════════════════════════════════
14
+
15
+ /**
16
+ * Inspect a workflow output object and classify its shape.
17
+ *
18
+ * @param {*} output - Workflow output value
19
+ * @returns {{ type: string, [key: string]: any }}
20
+ * type is one of: 'scalar', 'array', 'comparison', 'text', 'metrics'
21
+ */
22
+ function detectOutputShape(output) {
23
+ if (output == null || typeof output !== 'object') {
24
+ return { type: 'scalar', value: output };
25
+ }
26
+
27
+ const keys = Object.keys(output);
28
+
29
+ // Array-of-objects pattern: results, comparison, similarities, etc.
30
+ for (const key of keys) {
31
+ const val = output[key];
32
+ if (Array.isArray(val) && val.length > 0 && typeof val[0] === 'object' && val[0] !== null) {
33
+ const columns = Object.keys(val[0]);
34
+ return { type: 'array', arrayKey: key, columns, totalRows: val.length };
35
+ }
36
+ }
37
+
38
+ // Comparison objects: model_a / model_b or similar side-by-side objects
39
+ const objKeys = keys.filter(k =>
40
+ output[k] != null && typeof output[k] === 'object' && !Array.isArray(output[k])
41
+ );
42
+ if (objKeys.length >= 2) {
43
+ return { type: 'comparison', objectKeys: objKeys, metricKeys: keys.filter(k => !objKeys.includes(k)) };
44
+ }
45
+
46
+ // Text-heavy: long string fields (summary, report, answer)
47
+ const textKeys = keys.filter(k => typeof output[k] === 'string' && output[k].length > 100);
48
+ if (textKeys.length > 0) {
49
+ return { type: 'text', textKeys, metricKeys: keys.filter(k => !textKeys.includes(k)) };
50
+ }
51
+
52
+ // Flat key-value metrics
53
+ return { type: 'metrics', keys };
54
+ }
55
+
56
+ // ════════════════════════════════════════════════════════════════════
57
+ // Value Path Resolution
58
+ // ════════════════════════════════════════════════════════════════════
59
+
60
+ /**
61
+ * Extract a nested value using dot-notation with optional array bracket syntax.
62
+ * Example paths: "model_a.similarity", "results[0].score", "report"
63
+ *
64
+ * @param {object} obj
65
+ * @param {string} dotPath
66
+ * @returns {*}
67
+ */
68
+ function resolveValuePath(obj, dotPath) {
69
+ if (!obj || !dotPath) return undefined;
70
+ return dotPath.split('.').reduce((current, segment) => {
71
+ if (current == null) return undefined;
72
+ const bracketMatch = segment.match(/^(\w+)\[(\d+)\]$/);
73
+ if (bracketMatch) {
74
+ const arr = current[bracketMatch[1]];
75
+ return Array.isArray(arr) ? arr[parseInt(bracketMatch[2], 10)] : undefined;
76
+ }
77
+ return current[segment];
78
+ }, obj);
79
+ }
80
+
81
+ // ════════════════════════════════════════════════════════════════════
82
+ // Helpers
83
+ // ════════════════════════════════════════════════════════════════════
84
+
85
+ /**
86
+ * Consistently convert a value to a display string.
87
+ */
88
+ function stringify(val) {
89
+ if (val == null) return '';
90
+ if (typeof val === 'number') return val % 1 === 0 ? String(val) : val.toFixed(4);
91
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
92
+ if (typeof val === 'object') {
93
+ const s = JSON.stringify(val);
94
+ return s.length > 80 ? s.slice(0, 77) + '...' : s;
95
+ }
96
+ return String(val);
97
+ }
98
+
99
+ /**
100
+ * Escape a value for CSV output: quote if it contains commas, quotes, or newlines.
101
+ */
102
+ function csvEscape(val) {
103
+ const s = stringify(val);
104
+ if (s.includes(',') || s.includes('"') || s.includes('\n')) {
105
+ return '"' + s.replace(/"/g, '""') + '"';
106
+ }
107
+ return s;
108
+ }
109
+
110
+ /**
111
+ * Pretty-print a label/value pair for text output.
112
+ */
113
+ function labelLine(key, val) {
114
+ return ` ${pc.dim(key + ':')} ${typeof val === 'number' ? pc.cyan(stringify(val)) : stringify(val)}`;
115
+ }
116
+
117
+ // ════════════════════════════════════════════════════════════════════
118
+ // Format: Table
119
+ // ════════════════════════════════════════════════════════════════════
120
+
121
+ function formatAsTable(output, hints) {
122
+ const shape = detectOutputShape(output);
123
+
124
+ if (shape.type === 'array') {
125
+ const key = hints.arrayField || shape.arrayKey;
126
+ const data = output[key] || [];
127
+ if (data.length === 0) return '(empty results)';
128
+ const columns = hints.columns || shape.columns;
129
+ const headers = columns;
130
+ const rows = data.map(row => columns.map(col => stringify(row[col])));
131
+ let result = '';
132
+
133
+ // Show non-array metrics above the table
134
+ const metricKeys = Object.keys(output).filter(k => k !== key && !Array.isArray(output[k]));
135
+ if (metricKeys.length > 0) {
136
+ result += metricKeys.map(k => labelLine(k, output[k])).join('\n') + '\n\n';
137
+ }
138
+
139
+ result += formatTable(headers, rows);
140
+ return result;
141
+ }
142
+
143
+ if (shape.type === 'comparison') {
144
+ const keys = shape.objectKeys;
145
+ const allFields = new Set();
146
+ keys.forEach(k => {
147
+ if (output[k] && typeof output[k] === 'object') {
148
+ Object.keys(output[k]).forEach(f => allFields.add(f));
149
+ }
150
+ });
151
+ const columns = ['', ...keys];
152
+ const rows = [...allFields].map(field => [
153
+ field,
154
+ ...keys.map(k => stringify(output[k]?.[field])),
155
+ ]);
156
+
157
+ let result = '';
158
+ // Show non-object metrics above the table
159
+ const metricKeys = (shape.metricKeys || []).filter(k => output[k] != null);
160
+ if (metricKeys.length > 0) {
161
+ result += metricKeys.map(k => labelLine(k, output[k])).join('\n') + '\n\n';
162
+ }
163
+
164
+ result += formatTable(columns, rows);
165
+ return result;
166
+ }
167
+
168
+ // Fallback: key-value table
169
+ const rows = Object.entries(output).map(([k, v]) => [k, stringify(v)]);
170
+ return formatTable(['Field', 'Value'], rows);
171
+ }
172
+
173
+ // ════════════════════════════════════════════════════════════════════
174
+ // Format: Text
175
+ // ════════════════════════════════════════════════════════════════════
176
+
177
+ function formatAsText(output, hints) {
178
+ const shape = detectOutputShape(output);
179
+
180
+ if (shape.type === 'scalar') {
181
+ return String(output ?? '');
182
+ }
183
+
184
+ const lines = [];
185
+ const title = hints.title;
186
+ if (title) {
187
+ lines.push(pc.bold(title));
188
+ lines.push(pc.dim('─'.repeat(Math.min(title.length + 4, 50))));
189
+ lines.push('');
190
+ }
191
+
192
+ // Collect fields by type
193
+ const metricEntries = [];
194
+ const textEntries = [];
195
+ const arrayEntries = [];
196
+ const objectEntries = [];
197
+
198
+ for (const [k, v] of Object.entries(output)) {
199
+ if (v == null) continue;
200
+ if (Array.isArray(v)) {
201
+ arrayEntries.push([k, v]);
202
+ } else if (typeof v === 'object') {
203
+ objectEntries.push([k, v]);
204
+ } else if (typeof v === 'string' && v.length > 100) {
205
+ textEntries.push([k, v]);
206
+ } else {
207
+ metricEntries.push([k, v]);
208
+ }
209
+ }
210
+
211
+ // Metrics as labeled lines
212
+ if (metricEntries.length > 0) {
213
+ for (const [k, v] of metricEntries) {
214
+ lines.push(labelLine(k, v));
215
+ }
216
+ lines.push('');
217
+ }
218
+
219
+ // Comparison objects
220
+ if (objectEntries.length > 0) {
221
+ for (const [k, v] of objectEntries) {
222
+ lines.push(` ${pc.bold(k)}`);
223
+ for (const [subK, subV] of Object.entries(v)) {
224
+ lines.push(` ${pc.dim(subK + ':')} ${typeof subV === 'number' ? pc.cyan(stringify(subV)) : stringify(subV)}`);
225
+ }
226
+ lines.push('');
227
+ }
228
+ }
229
+
230
+ // Text fields
231
+ if (textEntries.length > 0) {
232
+ for (const [k, v] of textEntries) {
233
+ lines.push(` ${pc.bold(k)}`);
234
+ lines.push(` ${v}`);
235
+ lines.push('');
236
+ }
237
+ }
238
+
239
+ // Arrays: brief summary
240
+ if (arrayEntries.length > 0) {
241
+ for (const [k, v] of arrayEntries) {
242
+ lines.push(` ${pc.bold(k)} ${pc.dim(`(${v.length} items)`)}`);
243
+ const preview = v.slice(0, 3);
244
+ for (let i = 0; i < preview.length; i++) {
245
+ const item = preview[i];
246
+ if (typeof item === 'object' && item !== null) {
247
+ const firstVal = Object.values(item)[0];
248
+ const score = item.score != null ? ` ${pc.dim(`(${stringify(item.score)})`)}` : '';
249
+ lines.push(` ${pc.dim(`[${i + 1}]`)} ${stringify(firstVal)}${score}`);
250
+ } else {
251
+ lines.push(` ${pc.dim(`[${i + 1}]`)} ${stringify(item)}`);
252
+ }
253
+ }
254
+ if (v.length > 3) {
255
+ lines.push(` ${pc.dim(`... and ${v.length - 3} more`)}`);
256
+ }
257
+ lines.push('');
258
+ }
259
+ }
260
+
261
+ return lines.join('\n');
262
+ }
263
+
264
+ // ════════════════════════════════════════════════════════════════════
265
+ // Format: Markdown
266
+ // ════════════════════════════════════════════════════════════════════
267
+
268
+ function formatAsMarkdown(output, hints) {
269
+ const shape = detectOutputShape(output);
270
+ const lines = [];
271
+
272
+ const title = hints.title || 'Workflow Output';
273
+ lines.push(`## ${title}`);
274
+ lines.push('');
275
+
276
+ // Scalar metrics
277
+ const metricEntries = [];
278
+ const textEntries = [];
279
+ const arrayEntries = [];
280
+ const objectEntries = [];
281
+
282
+ for (const [k, v] of Object.entries(output)) {
283
+ if (v == null) continue;
284
+ if (Array.isArray(v)) {
285
+ arrayEntries.push([k, v]);
286
+ } else if (typeof v === 'object') {
287
+ objectEntries.push([k, v]);
288
+ } else if (typeof v === 'string' && v.length > 100) {
289
+ textEntries.push([k, v]);
290
+ } else {
291
+ metricEntries.push([k, v]);
292
+ }
293
+ }
294
+
295
+ if (metricEntries.length > 0) {
296
+ for (const [k, v] of metricEntries) {
297
+ lines.push(`- **${k}:** ${stringify(v)}`);
298
+ }
299
+ lines.push('');
300
+ }
301
+
302
+ // Object entries as sub-tables or nested lists
303
+ if (objectEntries.length > 0) {
304
+ if (shape.type === 'comparison') {
305
+ // Render as a comparison table
306
+ const keys = objectEntries.map(([k]) => k);
307
+ const allFields = new Set();
308
+ objectEntries.forEach(([, v]) => Object.keys(v).forEach(f => allFields.add(f)));
309
+ lines.push(`| | ${keys.join(' | ')} |`);
310
+ lines.push(`| --- | ${keys.map(() => '---').join(' | ')} |`);
311
+ for (const field of allFields) {
312
+ const vals = objectEntries.map(([, v]) => stringify(v[field]));
313
+ lines.push(`| **${field}** | ${vals.join(' | ')} |`);
314
+ }
315
+ lines.push('');
316
+ } else {
317
+ for (const [k, v] of objectEntries) {
318
+ lines.push(`### ${k}`);
319
+ lines.push('');
320
+ for (const [subK, subV] of Object.entries(v)) {
321
+ lines.push(`- **${subK}:** ${stringify(subV)}`);
322
+ }
323
+ lines.push('');
324
+ }
325
+ }
326
+ }
327
+
328
+ // Arrays as markdown tables
329
+ for (const [k, v] of arrayEntries) {
330
+ if (v.length === 0) continue;
331
+ if (typeof v[0] === 'object' && v[0] !== null) {
332
+ const cols = hints.columns || Object.keys(v[0]);
333
+ lines.push(`### ${k}`);
334
+ lines.push('');
335
+ lines.push(`| ${cols.join(' | ')} |`);
336
+ lines.push(`| ${cols.map(() => '---').join(' | ')} |`);
337
+ for (const row of v) {
338
+ lines.push(`| ${cols.map(c => stringify(row[c])).join(' | ')} |`);
339
+ }
340
+ lines.push('');
341
+ }
342
+ }
343
+
344
+ // Text fields
345
+ for (const [k, v] of textEntries) {
346
+ lines.push(`### ${k}`);
347
+ lines.push('');
348
+ lines.push(v);
349
+ lines.push('');
350
+ }
351
+
352
+ return lines.join('\n');
353
+ }
354
+
355
+ // ════════════════════════════════════════════════════════════════════
356
+ // Format: CSV
357
+ // ════════════════════════════════════════════════════════════════════
358
+
359
+ function formatAsCsv(output, hints) {
360
+ const shape = detectOutputShape(output);
361
+
362
+ if (shape.type === 'array') {
363
+ const key = hints.arrayField || shape.arrayKey;
364
+ const data = output[key] || [];
365
+ if (data.length === 0) return '';
366
+ const columns = hints.columns || shape.columns;
367
+ const headerLine = columns.join(',');
368
+ const dataLines = data.map(row => columns.map(c => csvEscape(row[c])).join(','));
369
+ return [headerLine, ...dataLines].join('\n');
370
+ }
371
+
372
+ if (shape.type === 'comparison') {
373
+ const keys = shape.objectKeys;
374
+ const allFields = new Set();
375
+ keys.forEach(k => {
376
+ if (output[k] && typeof output[k] === 'object') {
377
+ Object.keys(output[k]).forEach(f => allFields.add(f));
378
+ }
379
+ });
380
+ const headerLine = ['field', ...keys].join(',');
381
+ const dataLines = [...allFields].map(field =>
382
+ [csvEscape(field), ...keys.map(k => csvEscape(output[k]?.[field]))].join(',')
383
+ );
384
+ return [headerLine, ...dataLines].join('\n');
385
+ }
386
+
387
+ // Fallback: key,value
388
+ const headerLine = 'field,value';
389
+ const dataLines = Object.entries(output).map(([k, v]) =>
390
+ [csvEscape(k), csvEscape(v)].join(',')
391
+ );
392
+ return [headerLine, ...dataLines].join('\n');
393
+ }
394
+
395
+ // ════════════════════════════════════════════════════════════════════
396
+ // Main Dispatcher
397
+ // ════════════════════════════════════════════════════════════════════
398
+
399
+ /**
400
+ * Format workflow output in the requested format.
401
+ *
402
+ * @param {*} output - The workflow output object
403
+ * @param {string} format - One of: json, table, markdown, text, csv, value:<path>
404
+ * @param {object} [hints={}] - Optional formatter hints from workflow definition
405
+ * @returns {string}
406
+ */
407
+ function formatWorkflowOutput(output, format, hints = {}) {
408
+ if (!format) format = 'json';
409
+
410
+ // Handle value:<path> extraction
411
+ if (format.startsWith('value:')) {
412
+ const path = format.slice(6);
413
+ const val = resolveValuePath(output, path);
414
+ if (val === undefined) return '';
415
+ return typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
416
+ }
417
+
418
+ switch (format) {
419
+ case 'json':
420
+ return JSON.stringify(output, null, 2);
421
+ case 'table':
422
+ return formatAsTable(output, hints);
423
+ case 'markdown':
424
+ return formatAsMarkdown(output, hints);
425
+ case 'text':
426
+ return formatAsText(output, hints);
427
+ case 'csv':
428
+ return formatAsCsv(output, hints);
429
+ default:
430
+ return JSON.stringify(output, null, 2);
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Pick the best auto-detected format for a given output shape.
436
+ *
437
+ * @param {*} output
438
+ * @param {object} [hints={}]
439
+ * @returns {string}
440
+ */
441
+ function autoDetectFormat(output, hints = {}) {
442
+ if (hints.default && FORMAT_TYPES.has(hints.default)) return hints.default;
443
+ const shape = detectOutputShape(output);
444
+ if (shape.type === 'array' || shape.type === 'comparison') return 'table';
445
+ return 'text';
446
+ }
447
+
448
+ module.exports = {
449
+ FORMAT_TYPES,
450
+ detectOutputShape,
451
+ resolveValuePath,
452
+ formatWorkflowOutput,
453
+ autoDetectFormat,
454
+ };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.vai');
8
+ const CACHE_PATH = path.join(CONFIG_DIR, 'workflow-input-cache.json');
9
+
10
+ /**
11
+ * Derive a stable cache key from a workflow name.
12
+ * Slugifies the name: lowercase, spaces/underscores to hyphens, strip non-alphanumeric.
13
+ *
14
+ * @param {string} name - Workflow name (e.g. "Code Review Assistant")
15
+ * @returns {string} slug (e.g. "code-review-assistant")
16
+ */
17
+ function slugify(name) {
18
+ return String(name)
19
+ .toLowerCase()
20
+ .replace(/[\s_]+/g, '-')
21
+ .replace(/[^a-z0-9-]/g, '')
22
+ .replace(/-+/g, '-')
23
+ .replace(/^-|-$/g, '');
24
+ }
25
+
26
+ /**
27
+ * Load the full cache file. Returns {} if file doesn't exist or is corrupt.
28
+ * @param {string} [cachePath] - Override for testing
29
+ * @returns {object}
30
+ */
31
+ function loadAllCaches(cachePath) {
32
+ const p = cachePath || CACHE_PATH;
33
+ try {
34
+ const raw = fs.readFileSync(p, 'utf-8');
35
+ const parsed = JSON.parse(raw);
36
+ return (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) ? parsed : {};
37
+ } catch {
38
+ return {};
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Write the full cache object to disk.
44
+ * @param {object} cache
45
+ * @param {string} [cachePath] - Override for testing
46
+ */
47
+ function writeAllCaches(cache, cachePath) {
48
+ const p = cachePath || CACHE_PATH;
49
+ const dir = path.dirname(p);
50
+ fs.mkdirSync(dir, { recursive: true });
51
+ fs.writeFileSync(p, JSON.stringify(cache, null, 2) + '\n', 'utf-8');
52
+ }
53
+
54
+ /**
55
+ * Load cached inputs for a specific workflow.
56
+ *
57
+ * @param {string} workflowId - Workflow name or slug
58
+ * @param {string} [cachePath] - Override for testing
59
+ * @returns {object} Cached inputs as { key: value } or {}
60
+ */
61
+ function loadInputCache(workflowId, cachePath) {
62
+ const slug = slugify(workflowId);
63
+ if (!slug) return {};
64
+ const all = loadAllCaches(cachePath);
65
+ const entry = all[slug];
66
+ return (entry && typeof entry === 'object' && !Array.isArray(entry)) ? entry : {};
67
+ }
68
+
69
+ /**
70
+ * Save inputs for a specific workflow. Merges into existing cache file.
71
+ *
72
+ * @param {string} workflowId - Workflow name or slug
73
+ * @param {object} inputs - Input key-value pairs to cache
74
+ * @param {string} [cachePath] - Override for testing
75
+ */
76
+ function saveInputCache(workflowId, inputs, cachePath) {
77
+ const slug = slugify(workflowId);
78
+ if (!slug) return;
79
+ if (!inputs || typeof inputs !== 'object') return;
80
+
81
+ const all = loadAllCaches(cachePath);
82
+ all[slug] = { ...inputs };
83
+ writeAllCaches(all, cachePath);
84
+ }
85
+
86
+ /**
87
+ * Clear cached inputs for a specific workflow, or all workflows if no ID given.
88
+ *
89
+ * @param {string} [workflowId] - Workflow name or slug. Omit to clear all.
90
+ * @param {string} [cachePath] - Override for testing
91
+ */
92
+ function clearInputCache(workflowId, cachePath) {
93
+ if (!workflowId) {
94
+ // Clear everything
95
+ writeAllCaches({}, cachePath);
96
+ return;
97
+ }
98
+ const slug = slugify(workflowId);
99
+ if (!slug) return;
100
+ const all = loadAllCaches(cachePath);
101
+ delete all[slug];
102
+ writeAllCaches(all, cachePath);
103
+ }
104
+
105
+ module.exports = {
106
+ CACHE_PATH,
107
+ slugify,
108
+ loadInputCache,
109
+ saveInputCache,
110
+ clearInputCache,
111
+ };
@@ -298,7 +298,7 @@ function scaffoldPackage(options) {
298
298
  if (tool === 'query' || tool === 'search') {
299
299
  sampleMocks[tool] = { results: [{ text: 'Sample result', score: 0.95 }], resultCount: 1 };
300
300
  } else if (tool === 'embed') {
301
- sampleMocks[tool] = { embedding: [0.1, 0.2, 0.3], model: 'voyage-3-large', dimensions: 3 };
301
+ sampleMocks[tool] = { embedding: [0.1, 0.2, 0.3], model: 'voyage-4-large', dimensions: 3 };
302
302
  } else if (tool === 'rerank') {
303
303
  sampleMocks[tool] = { results: [{ text: 'Reranked result', score: 0.98 }], resultCount: 1 };
304
304
  } else if (tool === 'generate') {