voyageai-cli 1.26.1 → 1.28.0
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/package.json +1 -1
- package/src/commands/config.js +33 -0
- package/src/commands/doctor.js +157 -14
- package/src/commands/mcp-server.js +4 -1
- package/src/commands/playground.js +212 -4
- package/src/commands/workflow.js +45 -0
- package/src/lib/api.js +40 -2
- package/src/lib/workflow.js +98 -2
- package/src/mcp/server.js +15 -2
- package/src/mcp/sse-transport.js +112 -0
- package/src/playground/icons/dark/128.png +0 -0
- package/src/playground/icons/dark/16.png +0 -0
- package/src/playground/icons/dark/256.png +0 -0
- package/src/playground/icons/dark/32.png +0 -0
- package/src/playground/icons/dark/64.png +0 -0
- package/src/playground/icons/light/128.png +0 -0
- package/src/playground/icons/light/16.png +0 -0
- package/src/playground/icons/light/256.png +0 -0
- package/src/playground/icons/light/32.png +0 -0
- package/src/playground/icons/light/64.png +0 -0
- package/src/playground/index.html +3377 -168
package/package.json
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -67,6 +67,39 @@ function registerConfig(program) {
|
|
|
67
67
|
const storedValue = key === 'default-dimensions' ? parseInt(value, 10) : value;
|
|
68
68
|
setConfigValue(internalKey, storedValue);
|
|
69
69
|
console.log(ui.success(`Set ${ui.cyan(key)} = ${SECRET_KEYS.has(internalKey) ? ui.dim(maskSecret(String(storedValue))) : storedValue}`));
|
|
70
|
+
|
|
71
|
+
// When setting an API key, auto-configure the matching base URL
|
|
72
|
+
if (internalKey === 'apiKey') {
|
|
73
|
+
const { identifyKey, ATLAS_API_BASE, VOYAGE_API_BASE } = require('../lib/api');
|
|
74
|
+
const keyInfo = identifyKey(storedValue);
|
|
75
|
+
const currentBase = getConfigValue('baseUrl');
|
|
76
|
+
|
|
77
|
+
if (keyInfo.type === 'atlas') {
|
|
78
|
+
if (currentBase !== ATLAS_API_BASE) {
|
|
79
|
+
setConfigValue('baseUrl', ATLAS_API_BASE);
|
|
80
|
+
console.log('');
|
|
81
|
+
console.log(ui.success(`Auto-configured base URL → ${ui.cyan(ATLAS_API_BASE)}`));
|
|
82
|
+
}
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log(` 🔑 ${ui.bold('MongoDB Atlas key detected')} (al-*)`);
|
|
85
|
+
console.log(` Endpoint: ${ui.cyan(ATLAS_API_BASE)}`);
|
|
86
|
+
console.log(` Atlas provides Voyage AI models with Atlas-native billing.`);
|
|
87
|
+
console.log(` All vai features work identically on both endpoints.`);
|
|
88
|
+
} else if (keyInfo.type === 'voyage') {
|
|
89
|
+
if (currentBase !== VOYAGE_API_BASE) {
|
|
90
|
+
setConfigValue('baseUrl', VOYAGE_API_BASE);
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log(ui.success(`Auto-configured base URL → ${ui.cyan(VOYAGE_API_BASE)}`));
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(` 🔑 ${ui.bold('Voyage AI key detected')} (pa-*)`);
|
|
96
|
+
console.log(` Endpoint: ${ui.cyan(VOYAGE_API_BASE)}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(` ⚠️ Unrecognized key prefix. Expected ${ui.cyan('al-*')} (Atlas) or ${ui.cyan('pa-*')} (Voyage AI).`);
|
|
100
|
+
console.log(` Make sure your base URL matches: ${ui.cyan('vai config set base-url <url>')}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
70
103
|
});
|
|
71
104
|
|
|
72
105
|
// ── config get [key] ──
|
package/src/commands/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ const os = require('os');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const pc = require('picocolors');
|
|
7
|
-
const { getConfigValue } = require('../lib/config');
|
|
7
|
+
const { getConfigValue, setConfigValue, CONFIG_DIR, CONFIG_PATH } = require('../lib/config');
|
|
8
8
|
const { getApiBase } = require('../lib/api');
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -240,15 +240,83 @@ async function checkConfig() {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// ── Fix functions (for --fix mode) ──
|
|
244
|
+
|
|
245
|
+
async function fixApiKey() {
|
|
246
|
+
const readline = require('readline');
|
|
247
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
248
|
+
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
console.log(pc.cyan('\n Fixing: Voyage AI API Key'));
|
|
251
|
+
console.log(pc.dim(' Get a key at: https://dash.voyageai.com/api-keys\n'));
|
|
252
|
+
|
|
253
|
+
rl.question(' Enter your Voyage AI API key: ', (key) => {
|
|
254
|
+
rl.close();
|
|
255
|
+
const trimmed = (key || '').trim();
|
|
256
|
+
if (!trimmed) {
|
|
257
|
+
console.log(pc.yellow(' Skipped (no key entered)'));
|
|
258
|
+
resolve(false);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
setConfigValue('apiKey', trimmed);
|
|
263
|
+
// Also set in env for the current session so the connection check passes
|
|
264
|
+
process.env.VOYAGE_API_KEY = trimmed;
|
|
265
|
+
console.log(pc.green(' ✓ API key saved to ~/.vai/config.json'));
|
|
266
|
+
resolve(true);
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.log(pc.red(` ✗ Failed to save: ${err.message}`));
|
|
269
|
+
resolve(false);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function fixConfigPermissions() {
|
|
276
|
+
try {
|
|
277
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
278
|
+
console.log(pc.green(' ✓ Fixed ~/.vai/config.json permissions to 600'));
|
|
279
|
+
return true;
|
|
280
|
+
} catch (err) {
|
|
281
|
+
console.log(pc.red(` ✗ Failed to fix permissions: ${err.message}`));
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function fixConfigDir() {
|
|
287
|
+
try {
|
|
288
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
289
|
+
console.log(pc.green(' ✓ Created ~/.vai/ directory'));
|
|
290
|
+
return true;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.log(pc.red(` ✗ Failed to create directory: ${err.message}`));
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function fixPdfParse() {
|
|
298
|
+
const { execSync } = require('child_process');
|
|
299
|
+
console.log(pc.cyan('\n Installing pdf-parse...'));
|
|
300
|
+
try {
|
|
301
|
+
execSync('npm install pdf-parse', { stdio: 'pipe' });
|
|
302
|
+
console.log(pc.green(' ✓ pdf-parse installed'));
|
|
303
|
+
return true;
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.log(pc.red(` ✗ Failed to install: ${err.message}`));
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
243
310
|
async function runDoctor(options = {}) {
|
|
244
|
-
const { json, verbose } = options;
|
|
245
|
-
|
|
311
|
+
const { json, verbose, fix } = options;
|
|
312
|
+
|
|
246
313
|
console.log(pc.bold('\n🩺 Voyage AI CLI Health Check\n'));
|
|
247
|
-
|
|
314
|
+
|
|
248
315
|
const results = {};
|
|
249
316
|
let hasError = false;
|
|
250
317
|
let hasWarning = false;
|
|
251
|
-
|
|
318
|
+
const fixable = [];
|
|
319
|
+
|
|
252
320
|
// Run all checks
|
|
253
321
|
const checks = [
|
|
254
322
|
{ key: 'node', fn: checkNodeVersion },
|
|
@@ -258,12 +326,12 @@ async function runDoctor(options = {}) {
|
|
|
258
326
|
{ key: 'pdfParse', fn: checkPdfParse },
|
|
259
327
|
{ key: 'config', fn: checkConfig },
|
|
260
328
|
];
|
|
261
|
-
|
|
329
|
+
|
|
262
330
|
for (const { key, fn } of checks) {
|
|
263
331
|
const check = CHECKS[key];
|
|
264
332
|
const result = await fn();
|
|
265
333
|
results[key] = result;
|
|
266
|
-
|
|
334
|
+
|
|
267
335
|
// Determine status icon
|
|
268
336
|
let icon;
|
|
269
337
|
if (result.ok === true) {
|
|
@@ -276,15 +344,23 @@ async function runDoctor(options = {}) {
|
|
|
276
344
|
icon = warnMark();
|
|
277
345
|
hasWarning = true;
|
|
278
346
|
}
|
|
279
|
-
|
|
347
|
+
|
|
280
348
|
// Print result
|
|
281
349
|
console.log(` ${icon} ${pc.bold(check.name)}: ${result.message}`);
|
|
282
|
-
|
|
350
|
+
|
|
283
351
|
if (result.hint && (verbose || result.ok === false)) {
|
|
284
352
|
console.log(` ${pc.dim(result.hint)}`);
|
|
285
353
|
}
|
|
354
|
+
|
|
355
|
+
// Track fixable issues
|
|
356
|
+
if (result.ok !== true) {
|
|
357
|
+
if (key === 'apiKey' && result.ok === false) fixable.push('apiKey');
|
|
358
|
+
if (key === 'pdfParse' && result.ok === null) fixable.push('pdfParse');
|
|
359
|
+
if (key === 'config' && result.message && result.message.includes('permissions')) fixable.push('configPerms');
|
|
360
|
+
if (key === 'config' && result.ok === null) fixable.push('configDir');
|
|
361
|
+
}
|
|
286
362
|
}
|
|
287
|
-
|
|
363
|
+
|
|
288
364
|
// Summary
|
|
289
365
|
console.log('');
|
|
290
366
|
if (hasError) {
|
|
@@ -294,7 +370,43 @@ async function runDoctor(options = {}) {
|
|
|
294
370
|
} else {
|
|
295
371
|
console.log(pc.green(' ✓ All checks passed. vai is ready to use!\n'));
|
|
296
372
|
}
|
|
297
|
-
|
|
373
|
+
|
|
374
|
+
// --fix mode: attempt automatic repairs
|
|
375
|
+
if (fix && fixable.length > 0) {
|
|
376
|
+
console.log(pc.bold(' 🔧 Attempting fixes...\n'));
|
|
377
|
+
let fixed = 0;
|
|
378
|
+
|
|
379
|
+
for (const item of fixable) {
|
|
380
|
+
if (item === 'configDir') {
|
|
381
|
+
if (fixConfigDir()) fixed++;
|
|
382
|
+
}
|
|
383
|
+
if (item === 'apiKey') {
|
|
384
|
+
if (await fixApiKey()) fixed++;
|
|
385
|
+
}
|
|
386
|
+
if (item === 'configPerms') {
|
|
387
|
+
if (fixConfigPermissions()) fixed++;
|
|
388
|
+
}
|
|
389
|
+
if (item === 'pdfParse') {
|
|
390
|
+
if (await fixPdfParse()) fixed++;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
console.log('');
|
|
395
|
+
if (fixed > 0) {
|
|
396
|
+
console.log(pc.green(` ✓ Fixed ${fixed} issue${fixed === 1 ? '' : 's'}. Run ${pc.bold('vai doctor')} again to verify.\n`));
|
|
397
|
+
} else {
|
|
398
|
+
console.log(pc.yellow(' No issues were fixed. See hints above for manual steps.\n'));
|
|
399
|
+
}
|
|
400
|
+
return 0;
|
|
401
|
+
} else if (fix && fixable.length === 0 && (hasError || hasWarning)) {
|
|
402
|
+
console.log(pc.dim(' No auto-fixable issues found. See hints above for manual steps.\n'));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Suggest --fix if there are fixable issues and not in fix mode
|
|
406
|
+
if (!fix && fixable.length > 0) {
|
|
407
|
+
console.log(pc.dim(` Tip: run ${pc.bold('vai doctor --fix')} to attempt automatic repairs\n`));
|
|
408
|
+
}
|
|
409
|
+
|
|
298
410
|
// Suggest next steps
|
|
299
411
|
if (!hasError) {
|
|
300
412
|
console.log(pc.dim(' Next steps:'));
|
|
@@ -302,11 +414,11 @@ async function runDoctor(options = {}) {
|
|
|
302
414
|
console.log(pc.dim(' vai quickstart — Zero-to-search tutorial'));
|
|
303
415
|
console.log(pc.dim(' vai explain — Learn key concepts\n'));
|
|
304
416
|
}
|
|
305
|
-
|
|
417
|
+
|
|
306
418
|
if (json) {
|
|
307
419
|
console.log(JSON.stringify(results, null, 2));
|
|
308
420
|
}
|
|
309
|
-
|
|
421
|
+
|
|
310
422
|
return hasError ? 1 : 0;
|
|
311
423
|
}
|
|
312
424
|
|
|
@@ -316,10 +428,41 @@ function register(program) {
|
|
|
316
428
|
.description('Run health checks on your vai setup')
|
|
317
429
|
.option('--json', 'Output results as JSON')
|
|
318
430
|
.option('-v, --verbose', 'Show hints for all checks')
|
|
431
|
+
.option('-f, --fix', 'Attempt to fix issues automatically')
|
|
319
432
|
.action(async (options) => {
|
|
320
433
|
const exitCode = await runDoctor(options);
|
|
321
434
|
if (exitCode !== 0) process.exit(exitCode);
|
|
322
435
|
});
|
|
323
436
|
}
|
|
324
437
|
|
|
325
|
-
|
|
438
|
+
/**
|
|
439
|
+
* Run all checks programmatically and return structured results.
|
|
440
|
+
* Used by the playground /api/doctor endpoint.
|
|
441
|
+
*/
|
|
442
|
+
async function runChecks() {
|
|
443
|
+
const checks = [
|
|
444
|
+
{ key: 'node', fn: checkNodeVersion },
|
|
445
|
+
{ key: 'apiKey', fn: checkApiKey },
|
|
446
|
+
{ key: 'apiConnection', fn: checkApiConnection },
|
|
447
|
+
{ key: 'mongodb', fn: checkMongoDB },
|
|
448
|
+
{ key: 'pdfParse', fn: checkPdfParse },
|
|
449
|
+
{ key: 'config', fn: checkConfig },
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
const results = {};
|
|
453
|
+
for (const { key, fn } of checks) {
|
|
454
|
+
const result = await fn();
|
|
455
|
+
// Strip picocolors ANSI codes from messages for JSON output
|
|
456
|
+
const clean = (s) => typeof s === 'string' ? s.replace(/\x1b\[[0-9;]*m/g, '') : s;
|
|
457
|
+
results[key] = {
|
|
458
|
+
name: CHECKS[key].name,
|
|
459
|
+
required: CHECKS[key].required,
|
|
460
|
+
ok: result.ok,
|
|
461
|
+
message: clean(result.message),
|
|
462
|
+
hint: result.hint ? clean(result.hint) : null,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return results;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
module.exports = { register, runDoctor, runChecks };
|
|
@@ -14,6 +14,7 @@ function registerMcpServer(program) {
|
|
|
14
14
|
.option('--host <address>', 'Bind address (http transport only)', '127.0.0.1')
|
|
15
15
|
.option('--db <name>', 'Default MongoDB database for tools')
|
|
16
16
|
.option('--collection <name>', 'Default collection for tools')
|
|
17
|
+
.option('--no-sse', 'Disable SSE transport (SSE is enabled by default for HTTP)')
|
|
17
18
|
.option('--verbose', 'Enable debug logging to stderr')
|
|
18
19
|
.action(async (opts) => {
|
|
19
20
|
if (opts.verbose) {
|
|
@@ -27,7 +28,9 @@ function registerMcpServer(program) {
|
|
|
27
28
|
const { runStdioServer, runHttpServer } = require('../mcp/server');
|
|
28
29
|
|
|
29
30
|
if (opts.transport === 'http') {
|
|
30
|
-
|
|
31
|
+
// SSE is enabled by default for HTTP transport (required for n8n, etc.)
|
|
32
|
+
// Use --no-sse to disable if needed
|
|
33
|
+
await runHttpServer({ port: opts.port, host: opts.host, sse: opts.sse !== false });
|
|
31
34
|
} else if (opts.transport === 'stdio') {
|
|
32
35
|
await runStdioServer();
|
|
33
36
|
} else {
|
|
@@ -14,13 +14,16 @@ function registerPlayground(program) {
|
|
|
14
14
|
.command('playground')
|
|
15
15
|
.description('Launch interactive web playground for Voyage AI')
|
|
16
16
|
.option('-p, --port <port>', 'Port to serve on', '3333')
|
|
17
|
+
.option('--host <address>', 'Bind address', '127.0.0.1')
|
|
17
18
|
.option('--no-open', 'Skip auto-opening browser')
|
|
18
19
|
.action(async (opts) => {
|
|
19
20
|
const port = parseInt(opts.port, 10) || 3333;
|
|
21
|
+
const host = opts.host || '127.0.0.1';
|
|
20
22
|
const server = createPlaygroundServer();
|
|
21
23
|
|
|
22
|
-
server.listen(port, () => {
|
|
23
|
-
const
|
|
24
|
+
server.listen(port, host, () => {
|
|
25
|
+
const displayHost = host === '0.0.0.0' ? 'localhost' : host;
|
|
26
|
+
const url = `http://${displayHost}:${port}`;
|
|
24
27
|
console.log(`🧭 Playground running at ${url} — Press Ctrl+C to stop`);
|
|
25
28
|
|
|
26
29
|
if (opts.open !== false) {
|
|
@@ -309,6 +312,20 @@ function createPlaygroundServer() {
|
|
|
309
312
|
return;
|
|
310
313
|
}
|
|
311
314
|
|
|
315
|
+
// API: Doctor health checks
|
|
316
|
+
if (req.method === 'GET' && req.url === '/api/doctor') {
|
|
317
|
+
try {
|
|
318
|
+
const { runChecks } = require('./doctor');
|
|
319
|
+
const results = await runChecks();
|
|
320
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
321
|
+
res.end(JSON.stringify(results));
|
|
322
|
+
} catch (err) {
|
|
323
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
324
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
312
329
|
// API: Settings origins — where each config value comes from
|
|
313
330
|
if (req.method === 'GET' && req.url === '/api/settings/origins') {
|
|
314
331
|
const { resolveLLMConfig } = require('../lib/llm');
|
|
@@ -338,6 +355,54 @@ function createPlaygroundServer() {
|
|
|
338
355
|
return;
|
|
339
356
|
}
|
|
340
357
|
|
|
358
|
+
// API: List built-in workflows
|
|
359
|
+
if (req.method === 'GET' && req.url === '/api/workflows') {
|
|
360
|
+
try {
|
|
361
|
+
const { listBuiltinWorkflows } = require('../lib/workflow');
|
|
362
|
+
const workflows = listBuiltinWorkflows();
|
|
363
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
364
|
+
res.end(JSON.stringify({ workflows }));
|
|
365
|
+
} catch (err) {
|
|
366
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
367
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
368
|
+
}
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// API: List example workflows (must be before the :name route)
|
|
373
|
+
if (req.method === 'GET' && req.url === '/api/workflows/examples') {
|
|
374
|
+
try {
|
|
375
|
+
const { listExampleWorkflows } = require('../lib/workflow');
|
|
376
|
+
const examples = listExampleWorkflows();
|
|
377
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
378
|
+
res.end(JSON.stringify({ examples }));
|
|
379
|
+
} catch (err) {
|
|
380
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
381
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
382
|
+
}
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// API: Get a specific workflow by name
|
|
387
|
+
if (req.method === 'GET' && req.url?.startsWith('/api/workflows/')) {
|
|
388
|
+
const name = decodeURIComponent(req.url.replace('/api/workflows/', ''));
|
|
389
|
+
if (!name) {
|
|
390
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
391
|
+
res.end(JSON.stringify({ error: 'Workflow name is required' }));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const { loadWorkflow } = require('../lib/workflow');
|
|
396
|
+
const definition = loadWorkflow(name);
|
|
397
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
398
|
+
res.end(JSON.stringify({ definition }));
|
|
399
|
+
} catch (err) {
|
|
400
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
402
|
+
}
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
341
406
|
// API: Save chat config (POST) — persists to .vai.json
|
|
342
407
|
// Placed before generic POST handler so it doesn't require Voyage API key
|
|
343
408
|
if (req.method === 'POST' && req.url === '/api/chat/config') {
|
|
@@ -459,8 +524,37 @@ function createPlaygroundServer() {
|
|
|
459
524
|
opts: { systemPrompt, db: db || undefined, collection: collection || undefined },
|
|
460
525
|
})) {
|
|
461
526
|
if (event.type === 'tool_call') {
|
|
462
|
-
const { name, args, timeMs, error } = event.data;
|
|
463
|
-
|
|
527
|
+
const { name, args, timeMs, error, result } = event.data;
|
|
528
|
+
// Build a short human-readable summary of the tool result
|
|
529
|
+
let resultSummary = '';
|
|
530
|
+
if (!error && result) {
|
|
531
|
+
if (name === 'vai_query' || name === 'vai_search') {
|
|
532
|
+
const docs = result.results || result.documents || [];
|
|
533
|
+
resultSummary = `Found ${docs.length} result${docs.length !== 1 ? 's' : ''}`;
|
|
534
|
+
} else if (name === 'vai_collections') {
|
|
535
|
+
const colls = result.collections || [];
|
|
536
|
+
resultSummary = colls.length > 0
|
|
537
|
+
? colls.map(c => `<code>${c.name || c}</code>`).slice(0, 5).join(', ')
|
|
538
|
+
: 'No collections found';
|
|
539
|
+
} else if (name === 'vai_models') {
|
|
540
|
+
const models = result.models || [];
|
|
541
|
+
resultSummary = `${models.length} model${models.length !== 1 ? 's' : ''} available`;
|
|
542
|
+
} else if (name === 'vai_embed') {
|
|
543
|
+
const dims = result.dimensions || result.embedding?.length || '?';
|
|
544
|
+
resultSummary = `${dims}-dim vector`;
|
|
545
|
+
} else if (name === 'vai_similarity') {
|
|
546
|
+
const score = result.similarity ?? result.score;
|
|
547
|
+
resultSummary = score !== undefined ? `Score: ${Number(score).toFixed(4)}` : '';
|
|
548
|
+
} else if (name === 'vai_rerank') {
|
|
549
|
+
const items = result.results || [];
|
|
550
|
+
resultSummary = `Reranked ${items.length} result${items.length !== 1 ? 's' : ''}`;
|
|
551
|
+
} else if (name === 'vai_estimate') {
|
|
552
|
+
resultSummary = result.recommendation || '';
|
|
553
|
+
} else if (name === 'vai_explain' || name === 'vai_topics') {
|
|
554
|
+
resultSummary = result.title || result.topic || '';
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
res.write(`event: tool_call\ndata: ${JSON.stringify({ name, args, timeMs, error, resultSummary })}\n\n`);
|
|
464
558
|
} else if (event.type === 'chunk') {
|
|
465
559
|
res.write(`event: chunk\ndata: ${JSON.stringify({ text: event.data })}\n\n`);
|
|
466
560
|
} else if (event.type === 'done') {
|
|
@@ -645,6 +739,120 @@ function createPlaygroundServer() {
|
|
|
645
739
|
}));
|
|
646
740
|
return;
|
|
647
741
|
}
|
|
742
|
+
|
|
743
|
+
// API: Validate a workflow definition
|
|
744
|
+
if (req.url === '/api/workflows/validate') {
|
|
745
|
+
const { validateWorkflow } = require('../lib/workflow');
|
|
746
|
+
const { definition } = parsed;
|
|
747
|
+
if (!definition) {
|
|
748
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
749
|
+
res.end(JSON.stringify({ error: 'definition is required' }));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
const errors = validateWorkflow(definition);
|
|
753
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
754
|
+
res.end(JSON.stringify({ valid: errors.length === 0, errors }));
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// API: Get execution plan (layers + dependency graph)
|
|
759
|
+
if (req.url === '/api/workflows/plan') {
|
|
760
|
+
const { buildExecutionPlan, buildDependencyGraph, validateWorkflow } = require('../lib/workflow');
|
|
761
|
+
const { definition } = parsed;
|
|
762
|
+
if (!definition || !definition.steps) {
|
|
763
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
764
|
+
res.end(JSON.stringify({ error: 'definition with steps is required' }));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const errors = validateWorkflow(definition);
|
|
768
|
+
if (errors.length > 0) {
|
|
769
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
770
|
+
res.end(JSON.stringify({ error: 'Invalid workflow', errors }));
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const layers = buildExecutionPlan(definition.steps);
|
|
774
|
+
const graphMap = buildDependencyGraph(definition.steps);
|
|
775
|
+
// Convert Map<string, Set<string>> to plain object for JSON serialization
|
|
776
|
+
const graph = {};
|
|
777
|
+
for (const [stepId, deps] of graphMap) {
|
|
778
|
+
graph[stepId] = Array.from(deps);
|
|
779
|
+
}
|
|
780
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
781
|
+
res.end(JSON.stringify({ layers, graph }));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// API: Execute a workflow (streaming SSE)
|
|
786
|
+
if (req.url === '/api/workflows/execute') {
|
|
787
|
+
const { executeWorkflow, validateWorkflow: validateWf } = require('../lib/workflow');
|
|
788
|
+
const { definition, inputs } = parsed;
|
|
789
|
+
if (!definition) {
|
|
790
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
791
|
+
res.end(JSON.stringify({ error: 'definition is required' }));
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
const errors = validateWf(definition);
|
|
795
|
+
if (errors.length > 0) {
|
|
796
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
797
|
+
res.end(JSON.stringify({ error: 'Invalid workflow', errors }));
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Stream execution events as SSE
|
|
802
|
+
res.writeHead(200, {
|
|
803
|
+
'Content-Type': 'text/event-stream',
|
|
804
|
+
'Cache-Control': 'no-cache',
|
|
805
|
+
'Connection': 'keep-alive',
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
try {
|
|
809
|
+
const result = await executeWorkflow(definition, {
|
|
810
|
+
inputs: inputs || {},
|
|
811
|
+
onStepStart: (stepId, stepDef) => {
|
|
812
|
+
res.write(`event: step_start\ndata: ${JSON.stringify({
|
|
813
|
+
stepId,
|
|
814
|
+
name: stepDef.name || stepId,
|
|
815
|
+
tool: stepDef.tool,
|
|
816
|
+
})}\n\n`);
|
|
817
|
+
},
|
|
818
|
+
onStepComplete: (stepId, output, timeMs) => {
|
|
819
|
+
// Summarize output to avoid huge payloads
|
|
820
|
+
let summary = '';
|
|
821
|
+
if (output && typeof output === 'object') {
|
|
822
|
+
if (output.results) summary = `${output.results.length} results`;
|
|
823
|
+
else if (output.similarity !== undefined) summary = `similarity: ${Number(output.similarity).toFixed(4)}`;
|
|
824
|
+
else if (output.text) summary = output.text.slice(0, 100) + (output.text.length > 100 ? '...' : '');
|
|
825
|
+
else summary = JSON.stringify(output).slice(0, 200);
|
|
826
|
+
}
|
|
827
|
+
res.write(`event: step_complete\ndata: ${JSON.stringify({
|
|
828
|
+
stepId, timeMs, summary,
|
|
829
|
+
output: JSON.stringify(output).length < 5000 ? output : { _truncated: true, summary },
|
|
830
|
+
})}\n\n`);
|
|
831
|
+
},
|
|
832
|
+
onStepSkip: (stepId, reason) => {
|
|
833
|
+
res.write(`event: step_skip\ndata: ${JSON.stringify({ stepId, reason })}\n\n`);
|
|
834
|
+
},
|
|
835
|
+
onStepError: (stepId, error) => {
|
|
836
|
+
res.write(`event: step_error\ndata: ${JSON.stringify({
|
|
837
|
+
stepId,
|
|
838
|
+
error: error.message || String(error),
|
|
839
|
+
})}\n\n`);
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
res.write(`event: done\ndata: ${JSON.stringify({
|
|
844
|
+
output: result.output,
|
|
845
|
+
totalTimeMs: result.totalTimeMs,
|
|
846
|
+
layers: result.layers,
|
|
847
|
+
steps: result.steps,
|
|
848
|
+
})}\n\n`);
|
|
849
|
+
} catch (err) {
|
|
850
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
res.end();
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
648
856
|
}
|
|
649
857
|
|
|
650
858
|
// 404
|
package/src/commands/workflow.js
CHANGED
|
@@ -22,6 +22,45 @@ function collectInputs(pair, prev) {
|
|
|
22
22
|
return prev;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Interactively prompt the user for missing workflow inputs using @clack/prompts.
|
|
27
|
+
* Only prompts for inputs not already provided via --input flags.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} definition - Workflow definition
|
|
30
|
+
* @param {object} existingInputs - Inputs already provided via --input
|
|
31
|
+
* @returns {Promise<object>} Merged inputs (existing + prompted)
|
|
32
|
+
*/
|
|
33
|
+
async function promptForInputs(definition, existingInputs) {
|
|
34
|
+
const { buildInputSteps } = require('../lib/workflow');
|
|
35
|
+
const { createCLIRenderer } = require('../lib/wizard-cli');
|
|
36
|
+
const { runWizard } = require('../lib/wizard');
|
|
37
|
+
|
|
38
|
+
const allSteps = buildInputSteps(definition);
|
|
39
|
+
// Only prompt for inputs not already provided
|
|
40
|
+
const steps = allSteps.filter(s => !(s.id in existingInputs));
|
|
41
|
+
if (steps.length === 0) return existingInputs;
|
|
42
|
+
|
|
43
|
+
const renderer = createCLIRenderer({
|
|
44
|
+
title: `${definition.name || 'Workflow'} inputs`,
|
|
45
|
+
doneMessage: 'Inputs ready.',
|
|
46
|
+
showBackHint: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const { answers, cancelled } = await runWizard({
|
|
50
|
+
steps,
|
|
51
|
+
config: {},
|
|
52
|
+
renderer,
|
|
53
|
+
initial: {},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (cancelled) {
|
|
57
|
+
console.log(pc.dim('Cancelled.'));
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { ...existingInputs, ...answers };
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
/**
|
|
26
65
|
* Register the workflow command on a Commander program.
|
|
27
66
|
* @param {import('commander').Command} program
|
|
@@ -43,6 +82,7 @@ function registerWorkflow(program) {
|
|
|
43
82
|
.option('--quiet', 'Suppress progress output', false)
|
|
44
83
|
.option('--dry-run', 'Show execution plan without running', false)
|
|
45
84
|
.option('--verbose', 'Show step details', false)
|
|
85
|
+
.option('--no-interactive', 'Disable interactive input prompting')
|
|
46
86
|
.action(async (file, opts) => {
|
|
47
87
|
const { loadWorkflow, executeWorkflow, buildExecutionPlan, validateWorkflow } = require('../lib/workflow');
|
|
48
88
|
|
|
@@ -64,6 +104,11 @@ function registerWorkflow(program) {
|
|
|
64
104
|
|
|
65
105
|
const workflowName = definition.name || file;
|
|
66
106
|
|
|
107
|
+
// Interactive prompting for missing inputs
|
|
108
|
+
if (opts.interactive !== false && process.stdin.isTTY) {
|
|
109
|
+
opts.input = await promptForInputs(definition, opts.input);
|
|
110
|
+
}
|
|
111
|
+
|
|
67
112
|
if (opts.dryRun) {
|
|
68
113
|
// Dry run: show plan
|
|
69
114
|
const layers = buildExecutionPlan(definition.steps);
|