voyageai-cli 1.28.0 → 1.29.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 +2 -1
- package/src/commands/app.js +15 -0
- package/src/commands/playground.js +638 -7
- package/src/commands/workflow.js +417 -14
- package/src/lib/explanations.js +88 -0
- package/src/lib/npm-utils.js +265 -0
- package/src/lib/workflow-registry.js +416 -0
- package/src/lib/workflow-scaffold.js +319 -0
- package/src/lib/workflow.js +433 -7
- package/src/playground/announcements.md +71 -0
- package/src/playground/icons/V.png +0 -0
- package/src/playground/index.html +2204 -94
- package/src/workflows/consistency-check.json +4 -0
- package/src/workflows/cost-analysis.json +4 -0
- package/src/workflows/enrich-and-ingest.json +56 -0
- package/src/workflows/intelligent-ingest.json +66 -0
- package/src/workflows/kb-health-report.json +45 -0
- package/src/workflows/multi-collection-search.json +4 -0
- package/src/workflows/research-and-summarize.json +4 -0
- package/src/workflows/search-with-fallback.json +66 -0
- package/src/workflows/smart-ingest.json +4 -0
|
@@ -5,6 +5,79 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { exec } = require('child_process');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Load announcements from the markdown file.
|
|
10
|
+
* Format: Each announcement is separated by `---` and has YAML-like metadata
|
|
11
|
+
* followed by a ## title and description paragraph.
|
|
12
|
+
*/
|
|
13
|
+
function loadAnnouncementsFromMarkdown() {
|
|
14
|
+
const mdPath = path.join(__dirname, '..', 'playground', 'announcements.md');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(mdPath)) {
|
|
17
|
+
console.warn('Announcements file not found:', mdPath);
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
22
|
+
|
|
23
|
+
// Split by --- separator (skip the header section before first ---)
|
|
24
|
+
const sections = content.split(/\n---\n/).slice(1);
|
|
25
|
+
|
|
26
|
+
const announcements = [];
|
|
27
|
+
|
|
28
|
+
for (const section of sections) {
|
|
29
|
+
const lines = section.trim().split('\n');
|
|
30
|
+
const metadata = {};
|
|
31
|
+
let titleIndex = -1;
|
|
32
|
+
|
|
33
|
+
// Parse metadata lines (key: value format)
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const line = lines[i].trim();
|
|
36
|
+
|
|
37
|
+
// Stop at the title (## heading)
|
|
38
|
+
if (line.startsWith('## ')) {
|
|
39
|
+
titleIndex = i;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parse key: value
|
|
44
|
+
const match = line.match(/^([a-z_]+):\s*(.+)$/i);
|
|
45
|
+
if (match) {
|
|
46
|
+
metadata[match[1]] = match[2].trim();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (titleIndex === -1 || !metadata.id) continue;
|
|
51
|
+
|
|
52
|
+
// Extract title (## heading)
|
|
53
|
+
const title = lines[titleIndex].replace(/^##\s*/, '').trim();
|
|
54
|
+
|
|
55
|
+
// Extract description (paragraphs after the title)
|
|
56
|
+
const descriptionLines = [];
|
|
57
|
+
for (let i = titleIndex + 1; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i].trim();
|
|
59
|
+
if (line) descriptionLines.push(line);
|
|
60
|
+
}
|
|
61
|
+
const description = descriptionLines.join(' ');
|
|
62
|
+
|
|
63
|
+
announcements.push({
|
|
64
|
+
id: metadata.id,
|
|
65
|
+
title,
|
|
66
|
+
description,
|
|
67
|
+
badge: metadata.badge || 'Info',
|
|
68
|
+
published: metadata.published || new Date().toISOString().split('T')[0],
|
|
69
|
+
expires: metadata.expires || '2099-12-31',
|
|
70
|
+
cta: {
|
|
71
|
+
label: metadata.cta_label || 'Learn More',
|
|
72
|
+
action: metadata.cta_action || 'link',
|
|
73
|
+
target: metadata.cta_target || '#'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return announcements;
|
|
79
|
+
}
|
|
80
|
+
|
|
8
81
|
/**
|
|
9
82
|
* Register the playground command on a Commander program.
|
|
10
83
|
* @param {import('commander').Command} program
|
|
@@ -67,6 +140,10 @@ function createPlaygroundServer() {
|
|
|
67
140
|
// Chat history — scoped to the server lifetime (in-memory, no persistence)
|
|
68
141
|
let _chatHistory = null;
|
|
69
142
|
|
|
143
|
+
// Workflow store catalog cache (15 min TTL)
|
|
144
|
+
let _catalogCache = null;
|
|
145
|
+
let _catalogCacheTime = 0;
|
|
146
|
+
|
|
70
147
|
const server = http.createServer(async (req, res) => {
|
|
71
148
|
// CORS headers for local dev
|
|
72
149
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -105,6 +182,23 @@ function createPlaygroundServer() {
|
|
|
105
182
|
return;
|
|
106
183
|
}
|
|
107
184
|
|
|
185
|
+
// Serve V.png logo
|
|
186
|
+
if (req.method === 'GET' && req.url === '/icons/V.png') {
|
|
187
|
+
const logoPath = path.join(__dirname, '..', 'playground', 'icons', 'V.png');
|
|
188
|
+
if (fs.existsSync(logoPath)) {
|
|
189
|
+
const data = fs.readFileSync(logoPath);
|
|
190
|
+
res.writeHead(200, {
|
|
191
|
+
'Content-Type': 'image/png',
|
|
192
|
+
'Cache-Control': 'public, max-age=86400',
|
|
193
|
+
});
|
|
194
|
+
res.end(data);
|
|
195
|
+
} else {
|
|
196
|
+
res.writeHead(404);
|
|
197
|
+
res.end('Logo not found');
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
108
202
|
// Serve icon assets: /icons/{dark|light}/{size}.png
|
|
109
203
|
const iconMatch = req.url.match(/^\/icons\/(dark|light)\/(\d+)\.png$/);
|
|
110
204
|
if (req.method === 'GET' && iconMatch) {
|
|
@@ -312,6 +406,14 @@ function createPlaygroundServer() {
|
|
|
312
406
|
return;
|
|
313
407
|
}
|
|
314
408
|
|
|
409
|
+
// API: Version — return CLI package version
|
|
410
|
+
if (req.method === 'GET' && req.url === '/api/version') {
|
|
411
|
+
const pkg = require('../../package.json');
|
|
412
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
413
|
+
res.end(JSON.stringify({ version: pkg.version }));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
315
417
|
// API: Doctor health checks
|
|
316
418
|
if (req.method === 'GET' && req.url === '/api/doctor') {
|
|
317
419
|
try {
|
|
@@ -355,13 +457,353 @@ function createPlaygroundServer() {
|
|
|
355
457
|
return;
|
|
356
458
|
}
|
|
357
459
|
|
|
460
|
+
// API: Workflow Store catalog (cached 15 min)
|
|
461
|
+
if (req.method === 'GET' && req.url === '/api/workflows/catalog') {
|
|
462
|
+
const _catStart = Date.now();
|
|
463
|
+
try {
|
|
464
|
+
// Check cache
|
|
465
|
+
if (_catalogCache && (Date.now() - _catalogCacheTime < 15 * 60 * 1000)) {
|
|
466
|
+
console.log(`[catalog] served from cache in ${Date.now() - _catStart}ms`);
|
|
467
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
468
|
+
res.end(JSON.stringify(_catalogCache));
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const { getRegistry } = require('../lib/workflow-registry');
|
|
473
|
+
const registry = getRegistry({ force: true });
|
|
474
|
+
|
|
475
|
+
// Build set of installed package names
|
|
476
|
+
const installedNames = new Set();
|
|
477
|
+
for (const c of [...(registry.official || []), ...(registry.community || [])]) {
|
|
478
|
+
if (c.name) installedNames.add(c.name);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Try to fetch from npm
|
|
482
|
+
let npmWorkflows = [];
|
|
483
|
+
try {
|
|
484
|
+
const { searchNpm } = require('../lib/npm-utils');
|
|
485
|
+
const results = await searchNpm('', { limit: 50 });
|
|
486
|
+
npmWorkflows = results || [];
|
|
487
|
+
} catch (e) {
|
|
488
|
+
// npm unreachable — fall back to installed only
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Fetch registry metadata (for vai-workflow field, inputs, author)
|
|
492
|
+
// Only one request per package — no unpkg or downloads API on the critical path
|
|
493
|
+
const metadataCache = {};
|
|
494
|
+
await Promise.all(npmWorkflows.map(async (r) => {
|
|
495
|
+
try {
|
|
496
|
+
const encodedName = r.name.startsWith('@')
|
|
497
|
+
? `@${encodeURIComponent(r.name.slice(1))}`
|
|
498
|
+
: encodeURIComponent(r.name);
|
|
499
|
+
const regRes = await fetch(`https://registry.npmjs.org/${encodedName}/latest`, {
|
|
500
|
+
headers: { 'Accept': 'application/json' },
|
|
501
|
+
signal: AbortSignal.timeout(5000),
|
|
502
|
+
});
|
|
503
|
+
if (regRes.ok) {
|
|
504
|
+
metadataCache[r.name] = await regRes.json();
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
// Fall back to basic data from search
|
|
508
|
+
}
|
|
509
|
+
}));
|
|
510
|
+
|
|
511
|
+
// ── Lucide icon paths for workflow branding ──
|
|
512
|
+
// Curated subset of Lucide icons (lucide.dev, MIT) for the store.
|
|
513
|
+
// Each value is an SVG path (or multiple paths separated by a convention
|
|
514
|
+
// that the client renders inside a 24x24 viewBox with stroke).
|
|
515
|
+
const STORE_ICONS = {
|
|
516
|
+
trophy: 'M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22M18 2H6v7a6 6 0 0 0 12 0V2z',
|
|
517
|
+
search: 'M21 21l-4.3-4.3M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
|
|
518
|
+
'dollar-sign':'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
|
|
519
|
+
split: 'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22',
|
|
520
|
+
'file-search': 'M14 2v4a2 2 0 0 0 2 2h4M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM9.5 12.5a2.5 2.5 0 1 0 5 0 2.5 2.5 0 1 0-5 0M13.3 14.3 15 16',
|
|
521
|
+
database: 'M21 5c0 1.1-3.134 3-9 3S3 6.1 3 5M21 5c0-1.1-3.134-3-9-3S3 3.9 3 5M21 5v14c0 1.1-3.134 3-9 3s-9-1.9-9-3V5M21 12c0 1.1-3.134 3-9 3s-9-1.9-9-3',
|
|
522
|
+
activity: 'M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36-3.18-19.64A2 2 0 0 0 10.12 1h-.24a2 2 0 0 0-1.94 1.55L5.18 12H2',
|
|
523
|
+
globe: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z',
|
|
524
|
+
'shield-alert':'M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 .5-.87l7-4a1 1 0 0 1 1 0l7 4A1 1 0 0 1 20 6zM12 8v4M12 16h.01',
|
|
525
|
+
timer: 'M10 2h4M12 14l3-3M12 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',
|
|
526
|
+
'refresh-cw': 'M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16',
|
|
527
|
+
'flask-conical':'M10 2v7.527a2 2 0 0 1-.211.896L4.72 20.55a1 1 0 0 0 .9 1.45h12.76a1 1 0 0 0 .9-1.45l-5.069-10.127A2 2 0 0 1 14 9.527V2M8.5 2h7M7 16h10',
|
|
528
|
+
target: 'M12 12m-10 0a10 10 0 1 0 20 0 10 10 0 1 0-20 0M12 12m-6 0a6 6 0 1 0 12 0 6 6 0 1 0-12 0M12 12m-2 0a2 2 0 1 0 4 0 2 2 0 1 0-4 0',
|
|
529
|
+
code: 'M16 18l6-6-6-6M8 6l-6 6 6 6',
|
|
530
|
+
'clipboard-list':'M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2M9 2h6a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H9a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zM12 11h4M12 16h4M8 11h.01M8 16h.01',
|
|
531
|
+
layers: 'M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.84zM2 12l8.58 3.91a2 2 0 0 0 1.66 0L22 12M2 17l8.58 3.91a2 2 0 0 0 1.66 0L22 17',
|
|
532
|
+
'bar-chart-3': 'M12 20V10M18 20V4M6 20v-4',
|
|
533
|
+
'heart-pulse': 'M19.5 12.572l-7.5 7.428-7.5-7.428A5 5 0 0 1 7.5 5c1.8 0 3.3.9 4.5 2.7C13.2 5.9 14.7 5 16.5 5a5 5 0 0 1 3 9.572zM12 6l-1 4h4l-1 4',
|
|
534
|
+
brain: 'M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24 2.5 2.5 0 0 1 1.98-3A2.5 2.5 0 0 1 9.5 2zM14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-1.98-3A2.5 2.5 0 0 0 14.5 2z',
|
|
535
|
+
'check-circle':'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3',
|
|
536
|
+
zap: 'M13 2 3 14h9l-1 8 10-12h-9l1-8z',
|
|
537
|
+
package: 'M16.5 9.4l-9-5.19M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.27 6.96 12 12.01l8.73-5.05M12 22.08V12',
|
|
538
|
+
microscope: 'M6 18h8M3 22h18M14 22a7 7 0 1 0 0-14h-1M9 14h2M9 12a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2z',
|
|
539
|
+
sparkle: 'M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z',
|
|
540
|
+
scale: 'M16 3h5v5M8 3H3v5M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3M21 3l-7.828 7.828A4 4 0 0 0 12 13.7V22',
|
|
541
|
+
'file-text': 'M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7zM14 2v4a2 2 0 0 0 2 2h4M10 13h4M10 17h4M10 9h1',
|
|
542
|
+
filter: 'M3 6h18M7 12h10M10 18h4',
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// Category fallback icons (used when a workflow has no branding)
|
|
546
|
+
const CATEGORY_ICONS = {
|
|
547
|
+
retrieval: 'search',
|
|
548
|
+
analysis: 'bar-chart-3',
|
|
549
|
+
'domain-specific': 'target',
|
|
550
|
+
ingestion: 'database',
|
|
551
|
+
utility: 'zap',
|
|
552
|
+
integration: 'package',
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Default branding for the 20 official workflows
|
|
556
|
+
const DEFAULT_BRANDING = {
|
|
557
|
+
'model-shootout': { icon: 'trophy', color: '#0D9488' },
|
|
558
|
+
'asymmetric-search': { icon: 'split', color: '#00D4AA' },
|
|
559
|
+
'cost-optimizer': { icon: 'dollar-sign', color: '#F59E0B' },
|
|
560
|
+
'question-decomposition': { icon: 'sparkle', color: '#8B5CF6' },
|
|
561
|
+
'contract-clause-finder': { icon: 'file-search', color: '#1E40AF' },
|
|
562
|
+
'knowledge-base-bootstrap': { icon: 'database', color: '#059669' },
|
|
563
|
+
'embedding-drift-detector': { icon: 'activity', color: '#DC2626' },
|
|
564
|
+
'multilingual-search': { icon: 'globe', color: '#0EA5E9' },
|
|
565
|
+
'financial-risk-scanner': { icon: 'shield-alert', color: '#B45309' },
|
|
566
|
+
'doc-freshness': { icon: 'timer', color: '#4338CA' },
|
|
567
|
+
'incremental-sync': { icon: 'refresh-cw', color: '#15803D' },
|
|
568
|
+
'rag-ab-test': { icon: 'flask-conical', color: '#BE185D' },
|
|
569
|
+
'hybrid-precision-search': { icon: 'target', color: '#0891B2' },
|
|
570
|
+
'code-migration-helper': { icon: 'code', color: '#475569' },
|
|
571
|
+
'meeting-action-items': { icon: 'clipboard-list',color: '#7C2D12' },
|
|
572
|
+
'collection-overlap-audit': { icon: 'layers', color: '#6D28D9' },
|
|
573
|
+
'query-quality-scorer': { icon: 'microscope', color: '#9333EA' },
|
|
574
|
+
'clinical-protocol-match': { icon: 'heart-pulse', color: '#0F766E' },
|
|
575
|
+
'batch-quality-gate': { icon: 'check-circle', color: '#166534' },
|
|
576
|
+
'index-health-check': { icon: 'bar-chart-3', color: '#1D4ED8' },
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
// Static gradient/featured config
|
|
580
|
+
const GRADIENTS = {
|
|
581
|
+
'model-shootout': 'linear-gradient(135deg, #0D9488, #06B6D4)',
|
|
582
|
+
'asymmetric-search': 'linear-gradient(135deg, #00D4AA, #40E0FF)',
|
|
583
|
+
'cost-optimizer': 'linear-gradient(135deg, #F59E0B, #EF4444)',
|
|
584
|
+
'question-decomposition': 'linear-gradient(135deg, #8B5CF6, #EC4899)',
|
|
585
|
+
'contract-clause-finder': 'linear-gradient(135deg, #1E40AF, #7C3AED)',
|
|
586
|
+
'knowledge-base-bootstrap': 'linear-gradient(135deg, #059669, #10B981)',
|
|
587
|
+
'embedding-drift-detector': 'linear-gradient(135deg, #DC2626, #F97316)',
|
|
588
|
+
'multilingual-search': 'linear-gradient(135deg, #0EA5E9, #6366F1)',
|
|
589
|
+
'financial-risk-scanner': 'linear-gradient(135deg, #B45309, #D97706)',
|
|
590
|
+
'doc-freshness': 'linear-gradient(135deg, #4338CA, #7C3AED)',
|
|
591
|
+
'incremental-sync': 'linear-gradient(135deg, #15803D, #4ADE80)',
|
|
592
|
+
'rag-ab-test': 'linear-gradient(135deg, #BE185D, #F472B6)',
|
|
593
|
+
'hybrid-precision-search': 'linear-gradient(135deg, #0891B2, #22D3EE)',
|
|
594
|
+
'code-migration-helper': 'linear-gradient(135deg, #475569, #94A3B8)',
|
|
595
|
+
'meeting-action-items': 'linear-gradient(135deg, #7C2D12, #EA580C)',
|
|
596
|
+
'collection-overlap-audit': 'linear-gradient(135deg, #6D28D9, #A78BFA)',
|
|
597
|
+
'query-quality-scorer': 'linear-gradient(135deg, #9333EA, #C084FC)',
|
|
598
|
+
'clinical-protocol-match': 'linear-gradient(135deg, #0F766E, #2DD4BF)',
|
|
599
|
+
'batch-quality-gate': 'linear-gradient(135deg, #166534, #86EFAC)',
|
|
600
|
+
'index-health-check': 'linear-gradient(135deg, #1D4ED8, #60A5FA)',
|
|
601
|
+
};
|
|
602
|
+
const FEATURED = ['model-shootout', 'asymmetric-search', 'cost-optimizer'];
|
|
603
|
+
const DEFAULT_GRADIENT = 'linear-gradient(135deg, #334155, #64748B)';
|
|
604
|
+
|
|
605
|
+
const workflows = npmWorkflows.map(r => {
|
|
606
|
+
const shortName = (r.name || '').replace(/^@vaicli\/vai-workflow-/, '').replace(/^vai-workflow-/, '');
|
|
607
|
+
const meta = metadataCache[r.name]; // raw registry JSON
|
|
608
|
+
const vai = (meta && meta['vai-workflow']) || (meta && meta.vai) || r.vai || {};
|
|
609
|
+
const vaiAuthor = vai.author || null;
|
|
610
|
+
const version = (meta && meta.version) || r.version || '1.0.0';
|
|
611
|
+
|
|
612
|
+
// Author attribution: vai.author > package.json author
|
|
613
|
+
let author = { name: 'unknown' };
|
|
614
|
+
if (vaiAuthor && vaiAuthor.name) {
|
|
615
|
+
author = { name: vaiAuthor.name, url: vaiAuthor.url || undefined };
|
|
616
|
+
if (vaiAuthor.avatar) {
|
|
617
|
+
author.avatar = `https://unpkg.com/${r.name}@${version}/${vaiAuthor.avatar}`;
|
|
618
|
+
}
|
|
619
|
+
} else if (meta && meta.author) {
|
|
620
|
+
const rawAuthor = meta.author;
|
|
621
|
+
author = { name: typeof rawAuthor === 'string' ? rawAuthor : (rawAuthor.name || 'unknown') };
|
|
622
|
+
} else if (r.author) {
|
|
623
|
+
author = { name: r.author };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Assets: construct CDN URLs from vai.assets paths
|
|
627
|
+
const vaiAssets = vai.assets || {};
|
|
628
|
+
const assets = {};
|
|
629
|
+
if (vaiAssets.icon) assets.icon = `https://unpkg.com/${r.name}@${version}/${vaiAssets.icon}`;
|
|
630
|
+
if (vaiAssets.banner) assets.banner = `https://unpkg.com/${r.name}@${version}/${vaiAssets.banner}`;
|
|
631
|
+
if (vaiAssets.screenshots && Array.isArray(vaiAssets.screenshots)) {
|
|
632
|
+
assets.screenshots = vaiAssets.screenshots.map(s => `https://unpkg.com/${r.name}@${version}/${s}`);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Branding: vai.branding from package > DEFAULT_BRANDING > category fallback
|
|
636
|
+
const vaiBranding = vai.branding || {};
|
|
637
|
+
const defaultBrand = DEFAULT_BRANDING[shortName] || {};
|
|
638
|
+
const category = vai.category || 'utility';
|
|
639
|
+
const brandingIcon = vaiBranding.icon || defaultBrand.icon || CATEGORY_ICONS[category] || 'zap';
|
|
640
|
+
const brandingColor = vaiBranding.color || defaultBrand.color || '#64748B';
|
|
641
|
+
const branding = {
|
|
642
|
+
icon: brandingIcon,
|
|
643
|
+
color: brandingColor,
|
|
644
|
+
// Resolve the icon name to its SVG path data for client rendering
|
|
645
|
+
iconPath: STORE_ICONS[brandingIcon] || STORE_ICONS.zap,
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Inputs: extract from vai-workflow field (has inputs map), fall back to vai.inputs
|
|
649
|
+
const vaiWorkflowField = meta && meta['vai-workflow'] ? meta['vai-workflow'] : {};
|
|
650
|
+
const rawInputs = vaiWorkflowField.inputs || vai.inputs || {};
|
|
651
|
+
const inputs = Object.entries(rawInputs).map(([name, def]) => ({
|
|
652
|
+
name,
|
|
653
|
+
type: def.type || 'string',
|
|
654
|
+
required: !!def.required,
|
|
655
|
+
default: def.default !== undefined ? def.default : undefined,
|
|
656
|
+
description: def.description || '',
|
|
657
|
+
}));
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
name: shortName,
|
|
661
|
+
packageName: r.name,
|
|
662
|
+
version,
|
|
663
|
+
description: r.description || '',
|
|
664
|
+
category,
|
|
665
|
+
tags: vai.tags || [],
|
|
666
|
+
tools: vai.tools || [],
|
|
667
|
+
steps: vai.steps || (vai.tools || []).length || 0,
|
|
668
|
+
tools: vai.tools || [],
|
|
669
|
+
toolCount: (vai.tools || []).length,
|
|
670
|
+
tier: (r.name || '').startsWith('@vaicli/') ? 'official' : 'community',
|
|
671
|
+
downloads: 0,
|
|
672
|
+
featured: FEATURED.includes(shortName),
|
|
673
|
+
installed: installedNames.has(r.name),
|
|
674
|
+
gradient: GRADIENTS[shortName] || DEFAULT_GRADIENT,
|
|
675
|
+
branding,
|
|
676
|
+
author,
|
|
677
|
+
assets,
|
|
678
|
+
inputs,
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const result = { workflows, icons: STORE_ICONS, lastUpdated: new Date().toISOString() };
|
|
683
|
+
_catalogCache = result;
|
|
684
|
+
_catalogCacheTime = Date.now();
|
|
685
|
+
|
|
686
|
+
console.log(`[catalog] built fresh in ${Date.now() - _catStart}ms (${workflows.length} workflows)`);
|
|
687
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
688
|
+
res.end(JSON.stringify(result));
|
|
689
|
+
|
|
690
|
+
// Background enrichment: fetch real step counts + download stats
|
|
691
|
+
// Updates the cache silently — next client request gets enriched data
|
|
692
|
+
(async () => {
|
|
693
|
+
try {
|
|
694
|
+
let enriched = false;
|
|
695
|
+
await Promise.all(workflows.map(async (wf) => {
|
|
696
|
+
const [defRes, dlRes] = await Promise.all([
|
|
697
|
+
fetch(`https://unpkg.com/${wf.packageName}@${wf.version || 'latest'}/workflow.json`, {
|
|
698
|
+
headers: { 'Accept': 'application/json' },
|
|
699
|
+
signal: AbortSignal.timeout(8000),
|
|
700
|
+
}).catch(() => null),
|
|
701
|
+
fetch(`https://api.npmjs.org/downloads/point/last-month/${encodeURIComponent(wf.packageName)}`, {
|
|
702
|
+
headers: { 'Accept': 'application/json' },
|
|
703
|
+
signal: AbortSignal.timeout(8000),
|
|
704
|
+
}).catch(() => null),
|
|
705
|
+
]);
|
|
706
|
+
if (defRes && defRes.ok) {
|
|
707
|
+
try {
|
|
708
|
+
const def = await defRes.json();
|
|
709
|
+
if (def.steps && Array.isArray(def.steps) && def.steps.length > 0) {
|
|
710
|
+
wf.steps = def.steps.length;
|
|
711
|
+
enriched = true;
|
|
712
|
+
}
|
|
713
|
+
} catch {}
|
|
714
|
+
}
|
|
715
|
+
if (dlRes && dlRes.ok) {
|
|
716
|
+
try {
|
|
717
|
+
const dlData = await dlRes.json();
|
|
718
|
+
if (dlData.downloads > 0) {
|
|
719
|
+
wf.downloads = dlData.downloads;
|
|
720
|
+
enriched = true;
|
|
721
|
+
}
|
|
722
|
+
} catch {}
|
|
723
|
+
}
|
|
724
|
+
}));
|
|
725
|
+
if (enriched) {
|
|
726
|
+
_catalogCache = { ...result, workflows, lastUpdated: new Date().toISOString() };
|
|
727
|
+
}
|
|
728
|
+
} catch {}
|
|
729
|
+
})();
|
|
730
|
+
} catch (err) {
|
|
731
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
732
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
358
737
|
// API: List built-in workflows
|
|
359
738
|
if (req.method === 'GET' && req.url === '/api/workflows') {
|
|
360
739
|
try {
|
|
361
|
-
const {
|
|
362
|
-
const
|
|
740
|
+
const { getRegistry } = require('../lib/workflow-registry');
|
|
741
|
+
const registry = getRegistry({ force: true });
|
|
742
|
+
const workflows = registry.builtIn;
|
|
743
|
+
const mapPkg = (c, source) => ({
|
|
744
|
+
name: c.name,
|
|
745
|
+
description: c.pkg?.description || c.definition?.description || '',
|
|
746
|
+
version: c.pkg?.version,
|
|
747
|
+
author: typeof c.pkg?.author === 'string' ? c.pkg.author : c.pkg?.author?.name || '',
|
|
748
|
+
category: c.pkg?.vai?.category || 'utility',
|
|
749
|
+
tags: c.pkg?.vai?.tags || [],
|
|
750
|
+
tools: c.pkg?.vai?.tools || [],
|
|
751
|
+
source,
|
|
752
|
+
scope: c.scope,
|
|
753
|
+
});
|
|
754
|
+
// Include workflows that have a definition, even if they have non-fatal errors
|
|
755
|
+
// (e.g., "Missing vai field" is a warning, not a blocker)
|
|
756
|
+
const official = registry.official
|
|
757
|
+
.filter(c => c.definition)
|
|
758
|
+
.map(c => mapPkg(c, 'official'));
|
|
759
|
+
const community = registry.community
|
|
760
|
+
.filter(c => c.definition)
|
|
761
|
+
.map(c => mapPkg(c, 'community'));
|
|
762
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
763
|
+
res.end(JSON.stringify({ workflows, official, community }));
|
|
764
|
+
} catch (err) {
|
|
765
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
766
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
767
|
+
}
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// API: Community workflow operations
|
|
772
|
+
if (req.method === 'GET' && req.url === '/api/workflows/community') {
|
|
773
|
+
try {
|
|
774
|
+
const { getRegistry } = require('../lib/workflow-registry');
|
|
775
|
+
const registry = getRegistry({ force: true });
|
|
776
|
+
const mapPkg = (c) => ({
|
|
777
|
+
name: c.name,
|
|
778
|
+
description: c.pkg?.description || '',
|
|
779
|
+
version: c.pkg?.version,
|
|
780
|
+
author: typeof c.pkg?.author === 'string' ? c.pkg.author : c.pkg?.author?.name || '',
|
|
781
|
+
category: c.pkg?.vai?.category || 'utility',
|
|
782
|
+
tags: c.pkg?.vai?.tags || [],
|
|
783
|
+
valid: c.errors.length === 0,
|
|
784
|
+
errors: c.errors,
|
|
785
|
+
warnings: c.warnings,
|
|
786
|
+
});
|
|
787
|
+
const official = registry.official.map(mapPkg);
|
|
788
|
+
const community = registry.community.map(mapPkg);
|
|
363
789
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
364
|
-
res.end(JSON.stringify({
|
|
790
|
+
res.end(JSON.stringify({ official, community }));
|
|
791
|
+
} catch (err) {
|
|
792
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
793
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
794
|
+
}
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (req.method === 'GET' && req.url?.startsWith('/api/workflows/community/search?')) {
|
|
799
|
+
try {
|
|
800
|
+
const { searchNpm } = require('../lib/npm-utils');
|
|
801
|
+
const urlObj = new URL(req.url, `http://localhost`);
|
|
802
|
+
const query = urlObj.searchParams.get('q') || '';
|
|
803
|
+
const limit = parseInt(urlObj.searchParams.get('limit') || '10', 10);
|
|
804
|
+
const results = await searchNpm(query, { limit });
|
|
805
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
806
|
+
res.end(JSON.stringify({ results }));
|
|
365
807
|
} catch (err) {
|
|
366
808
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
367
809
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -383,7 +825,7 @@ function createPlaygroundServer() {
|
|
|
383
825
|
return;
|
|
384
826
|
}
|
|
385
827
|
|
|
386
|
-
// API: Get a specific workflow by name
|
|
828
|
+
// API: Get a specific workflow by name (built-in, community, or example)
|
|
387
829
|
if (req.method === 'GET' && req.url?.startsWith('/api/workflows/')) {
|
|
388
830
|
const name = decodeURIComponent(req.url.replace('/api/workflows/', ''));
|
|
389
831
|
if (!name) {
|
|
@@ -392,10 +834,10 @@ function createPlaygroundServer() {
|
|
|
392
834
|
return;
|
|
393
835
|
}
|
|
394
836
|
try {
|
|
395
|
-
const {
|
|
396
|
-
const
|
|
837
|
+
const { resolveWorkflow } = require('../lib/workflow-registry');
|
|
838
|
+
const resolved = resolveWorkflow(name);
|
|
397
839
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
398
|
-
res.end(JSON.stringify({ definition }));
|
|
840
|
+
res.end(JSON.stringify({ definition: resolved.definition, source: resolved.source, metadata: resolved.metadata }));
|
|
399
841
|
} catch (err) {
|
|
400
842
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
401
843
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -403,6 +845,148 @@ function createPlaygroundServer() {
|
|
|
403
845
|
return;
|
|
404
846
|
}
|
|
405
847
|
|
|
848
|
+
// API: Home announcements (loaded from markdown file)
|
|
849
|
+
if (req.method === 'GET' && req.url === '/api/home/announcements') {
|
|
850
|
+
try {
|
|
851
|
+
const announcements = loadAnnouncementsFromMarkdown();
|
|
852
|
+
|
|
853
|
+
// Filter out expired announcements
|
|
854
|
+
const now = new Date();
|
|
855
|
+
const activeAnnouncements = announcements.filter(a => {
|
|
856
|
+
const expires = new Date(a.expires);
|
|
857
|
+
return expires > now;
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
861
|
+
res.end(JSON.stringify({ announcements: activeAnnouncements }));
|
|
862
|
+
} catch (err) {
|
|
863
|
+
console.error('Failed to load announcements:', err);
|
|
864
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
865
|
+
res.end(JSON.stringify({ announcements: [] }));
|
|
866
|
+
}
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// API: Home releases
|
|
871
|
+
if (req.method === 'GET' && req.url === '/api/home/releases') {
|
|
872
|
+
try {
|
|
873
|
+
// Fetch from GitHub API with caching
|
|
874
|
+
const cacheKey = 'github-releases-cache';
|
|
875
|
+
const cached = global[cacheKey];
|
|
876
|
+
|
|
877
|
+
if (cached && Date.now() - cached.timestamp < 30 * 60 * 1000) {
|
|
878
|
+
// Use cached data if less than 30 minutes old
|
|
879
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
880
|
+
res.end(JSON.stringify(cached.data));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const https = require('https');
|
|
885
|
+
|
|
886
|
+
const fetchGitHub = () => new Promise((resolve, reject) => {
|
|
887
|
+
const options = {
|
|
888
|
+
hostname: 'api.github.com',
|
|
889
|
+
path: '/repos/mrlynn/voyageai-cli/releases?per_page=5',
|
|
890
|
+
method: 'GET',
|
|
891
|
+
headers: {
|
|
892
|
+
'User-Agent': 'VAI-Playground',
|
|
893
|
+
'Accept': 'application/vnd.github.v3+json'
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
const req = https.request(options, (response) => {
|
|
898
|
+
let data = '';
|
|
899
|
+
response.on('data', chunk => data += chunk);
|
|
900
|
+
response.on('end', () => {
|
|
901
|
+
if (response.statusCode === 200) {
|
|
902
|
+
try {
|
|
903
|
+
resolve(JSON.parse(data));
|
|
904
|
+
} catch (e) {
|
|
905
|
+
reject(new Error('Failed to parse GitHub response'));
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
console.error(`GitHub API error: ${response.statusCode} - ${data.substring(0, 200)}`);
|
|
909
|
+
reject(new Error(`GitHub API returned ${response.statusCode}`));
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
});
|
|
913
|
+
req.on('error', (err) => {
|
|
914
|
+
console.error('GitHub request error:', err.message);
|
|
915
|
+
reject(err);
|
|
916
|
+
});
|
|
917
|
+
req.setTimeout(10000, () => {
|
|
918
|
+
req.destroy();
|
|
919
|
+
reject(new Error('Request timeout'));
|
|
920
|
+
});
|
|
921
|
+
req.end();
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const githubReleases = await fetchGitHub();
|
|
925
|
+
|
|
926
|
+
// Parse release notes
|
|
927
|
+
const releases = githubReleases.map(release => {
|
|
928
|
+
let highlights = [];
|
|
929
|
+
|
|
930
|
+
if (release.body) {
|
|
931
|
+
// Extract bullet points from markdown body
|
|
932
|
+
const lines = release.body.split('\n');
|
|
933
|
+
highlights = lines
|
|
934
|
+
.filter(line => line.trim().startsWith('-') || line.trim().startsWith('*'))
|
|
935
|
+
.map(line => line.replace(/^[-*]\s*/, '').trim())
|
|
936
|
+
.filter(line => line.length > 0)
|
|
937
|
+
.slice(0, 5); // Max 5 highlights
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (highlights.length === 0) {
|
|
941
|
+
highlights = ['New features and improvements'];
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return {
|
|
945
|
+
version: release.tag_name || release.name,
|
|
946
|
+
date: release.published_at,
|
|
947
|
+
highlights,
|
|
948
|
+
url: release.html_url
|
|
949
|
+
};
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
const result = { releases };
|
|
953
|
+
|
|
954
|
+
// Cache the result
|
|
955
|
+
global[cacheKey] = {
|
|
956
|
+
data: result,
|
|
957
|
+
timestamp: Date.now()
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
961
|
+
res.end(JSON.stringify(result));
|
|
962
|
+
|
|
963
|
+
} catch (err) {
|
|
964
|
+
console.error('Failed to fetch GitHub releases:', err);
|
|
965
|
+
|
|
966
|
+
// Return fallback data
|
|
967
|
+
const fallback = {
|
|
968
|
+
releases: [
|
|
969
|
+
{
|
|
970
|
+
version: 'v1.0.0',
|
|
971
|
+
date: '2026-02-14T00:00:00Z',
|
|
972
|
+
highlights: [
|
|
973
|
+
'Initial release of VAI Playground',
|
|
974
|
+
'Support for all Voyage AI models',
|
|
975
|
+
'Interactive embedding visualization',
|
|
976
|
+
'Model comparison tools',
|
|
977
|
+
'Vector similarity analysis'
|
|
978
|
+
],
|
|
979
|
+
url: 'https://github.com/mrlynn/voyageai-cli/releases'
|
|
980
|
+
}
|
|
981
|
+
]
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
985
|
+
res.end(JSON.stringify(fallback));
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
|
|
406
990
|
// API: Save chat config (POST) — persists to .vai.json
|
|
407
991
|
// Placed before generic POST handler so it doesn't require Voyage API key
|
|
408
992
|
if (req.method === 'POST' && req.url === '/api/chat/config') {
|
|
@@ -444,6 +1028,53 @@ function createPlaygroundServer() {
|
|
|
444
1028
|
}
|
|
445
1029
|
|
|
446
1030
|
// Parse JSON body for POST routes
|
|
1031
|
+
// Community workflow install (no API key needed)
|
|
1032
|
+
if (req.method === 'POST' && req.url === '/api/workflows/community/install') {
|
|
1033
|
+
try {
|
|
1034
|
+
const body = await readBody(req);
|
|
1035
|
+
const { name } = JSON.parse(body);
|
|
1036
|
+
if (!name) {
|
|
1037
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1038
|
+
res.end(JSON.stringify({ error: 'Package name is required' }));
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const { installPackage, WORKFLOW_PREFIX, isWorkflowPackage } = require('../lib/npm-utils');
|
|
1042
|
+
const { validatePackage, clearRegistryCache } = require('../lib/workflow-registry');
|
|
1043
|
+
const packageName = name.startsWith('@') || name.startsWith(WORKFLOW_PREFIX) ? name : WORKFLOW_PREFIX + name;
|
|
1044
|
+
const result = installPackage(packageName);
|
|
1045
|
+
clearRegistryCache();
|
|
1046
|
+
_catalogCache = null; _catalogCacheTime = 0; // Invalidate store catalog cache
|
|
1047
|
+
let validation = null;
|
|
1048
|
+
if (result.path) {
|
|
1049
|
+
validation = validatePackage(result.path);
|
|
1050
|
+
}
|
|
1051
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1052
|
+
res.end(JSON.stringify({ success: true, version: result.version, path: result.path, validation: validation ? { valid: validation.errors.length === 0, errors: validation.errors, warnings: validation.warnings } : null }));
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1055
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Community workflow uninstall (no API key needed)
|
|
1061
|
+
if (req.method === 'DELETE' && req.url?.startsWith('/api/workflows/community/')) {
|
|
1062
|
+
try {
|
|
1063
|
+
const packageName = decodeURIComponent(req.url.replace('/api/workflows/community/', ''));
|
|
1064
|
+
const { uninstallPackage } = require('../lib/npm-utils');
|
|
1065
|
+
const { clearRegistryCache } = require('../lib/workflow-registry');
|
|
1066
|
+
uninstallPackage(packageName);
|
|
1067
|
+
clearRegistryCache();
|
|
1068
|
+
_catalogCache = null; _catalogCacheTime = 0; // Invalidate store catalog cache
|
|
1069
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1070
|
+
res.end(JSON.stringify({ success: true }));
|
|
1071
|
+
} catch (err) {
|
|
1072
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1073
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
447
1078
|
if (req.method === 'POST') {
|
|
448
1079
|
// Check for API key before processing any API calls
|
|
449
1080
|
const apiKeyConfigured = !!(process.env.VOYAGE_API_KEY || getConfigValue('apiKey'));
|