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.
- package/README.md +4 -4
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +3 -3
- package/src/commands/code-search.js +751 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/index-workspace.js +9 -5
- package/src/commands/playground.js +9 -1
- 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/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 +91 -1
- package/src/mcp/schemas/index.js +130 -0
- package/src/mcp/server.js +17 -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 +1 -12
- package/src/mcp/utils.js +20 -0
- package/src/playground/help/workflow-nodes.js +127 -2
- package/src/playground/index.html +1366 -24
- package/src/workflows/code-review.json +110 -0
- package/src/workflows/cost-analysis.json +5 -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/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-
|
|
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') {
|