voyageai-cli 1.22.0 → 1.23.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 +4 -2
- package/src/cli.js +4 -0
- package/src/commands/chat.js +503 -0
- package/src/commands/demo.js +75 -0
- package/src/commands/embed.js +10 -0
- package/src/commands/index.js +1 -1
- package/src/commands/init.js +34 -97
- package/src/commands/mcp-server.js +49 -0
- package/src/commands/ping.js +52 -0
- package/src/commands/pipeline.js +17 -3
- package/src/commands/playground.js +186 -0
- package/src/commands/purge.js +3 -1
- package/src/commands/refresh.js +3 -1
- package/src/commands/rerank.js +10 -0
- package/src/commands/scaffold.js +1 -2
- package/src/lib/chat.js +252 -0
- package/src/lib/codegen.js +5 -4
- package/src/lib/config.js +5 -1
- package/src/lib/cost.js +352 -0
- package/src/lib/explanations.js +260 -0
- package/src/lib/history.js +260 -0
- package/src/lib/llm.js +485 -0
- package/src/lib/preflight.js +281 -0
- package/src/lib/prompt.js +111 -0
- package/src/lib/wizard-cli.js +135 -0
- package/src/lib/wizard-steps-chat.js +171 -0
- package/src/lib/wizard-steps-init.js +174 -0
- package/src/lib/wizard.js +222 -0
- package/src/mcp/schemas/index.js +102 -0
- package/src/mcp/server.js +162 -0
- package/src/mcp/tools/embedding.js +67 -0
- package/src/mcp/tools/ingest.js +89 -0
- package/src/mcp/tools/management.js +132 -0
- package/src/mcp/tools/retrieval.js +209 -0
- package/src/mcp/tools/utility.js +219 -0
- package/src/playground/index.html +1195 -199
package/src/commands/init.js
CHANGED
|
@@ -2,50 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { MODEL_CATALOG } = require('../lib/catalog');
|
|
7
|
-
const { STRATEGIES } = require('../lib/chunker');
|
|
8
5
|
const { defaultProjectConfig, saveProject, findProjectFile, PROJECT_FILE } = require('../lib/project');
|
|
6
|
+
const { runWizard } = require('../lib/wizard');
|
|
7
|
+
const { createCLIRenderer } = require('../lib/wizard-cli');
|
|
8
|
+
const { initSteps } = require('../lib/wizard-steps-init');
|
|
9
9
|
const ui = require('../lib/ui');
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Prompt the user for input with a default value.
|
|
13
|
-
* @param {readline.Interface} rl
|
|
14
|
-
* @param {string} question
|
|
15
|
-
* @param {string} [defaultVal]
|
|
16
|
-
* @returns {Promise<string>}
|
|
17
|
-
*/
|
|
18
|
-
function ask(rl, question, defaultVal) {
|
|
19
|
-
const suffix = defaultVal ? ` ${ui.dim(`(${defaultVal})`)}` : '';
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
rl.question(` ${question}${suffix}: `, (answer) => {
|
|
22
|
-
resolve(answer.trim() || defaultVal || '');
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Prompt for a choice from a list.
|
|
29
|
-
* @param {readline.Interface} rl
|
|
30
|
-
* @param {string} question
|
|
31
|
-
* @param {string[]} choices
|
|
32
|
-
* @param {string} defaultVal
|
|
33
|
-
* @returns {Promise<string>}
|
|
34
|
-
*/
|
|
35
|
-
async function askChoice(rl, question, choices, defaultVal) {
|
|
36
|
-
console.log('');
|
|
37
|
-
for (let i = 0; i < choices.length; i++) {
|
|
38
|
-
const marker = choices[i] === defaultVal ? ui.cyan('→') : ' ';
|
|
39
|
-
console.log(` ${marker} ${i + 1}. ${choices[i]}`);
|
|
40
|
-
}
|
|
41
|
-
const answer = await ask(rl, question, defaultVal);
|
|
42
|
-
// Accept number or value
|
|
43
|
-
const num = parseInt(answer, 10);
|
|
44
|
-
if (num >= 1 && num <= choices.length) return choices[num - 1];
|
|
45
|
-
if (choices.includes(answer)) return answer;
|
|
46
|
-
return defaultVal;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
11
|
/**
|
|
50
12
|
* Register the init command on a Commander program.
|
|
51
13
|
* @param {import('commander').Command} program
|
|
@@ -72,7 +34,7 @@ function registerInit(program) {
|
|
|
72
34
|
|
|
73
35
|
// Non-interactive mode
|
|
74
36
|
if (opts.yes || opts.json) {
|
|
75
|
-
|
|
37
|
+
saveProject(defaults);
|
|
76
38
|
if (opts.json) {
|
|
77
39
|
console.log(JSON.stringify(defaults, null, 2));
|
|
78
40
|
} else if (!opts.quiet) {
|
|
@@ -81,71 +43,46 @@ function registerInit(program) {
|
|
|
81
43
|
return;
|
|
82
44
|
}
|
|
83
45
|
|
|
84
|
-
// Interactive mode
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
input: process.stdin,
|
|
93
|
-
output: process.stdout,
|
|
46
|
+
// Interactive mode — use wizard
|
|
47
|
+
const { answers, cancelled } = await runWizard({
|
|
48
|
+
steps: initSteps,
|
|
49
|
+
config: {},
|
|
50
|
+
renderer: createCLIRenderer({
|
|
51
|
+
title: '🚀 Initialize Voyage AI Project',
|
|
52
|
+
doneMessage: 'Project initialized!',
|
|
53
|
+
}),
|
|
94
54
|
});
|
|
95
55
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.filter(m => m.type === 'embedding' && !m.legacy && !m.unreleased)
|
|
100
|
-
.map(m => m.name);
|
|
101
|
-
const model = await askChoice(rl, 'Embedding model', embeddingModels, defaults.model);
|
|
102
|
-
|
|
103
|
-
// MongoDB settings
|
|
104
|
-
console.log('');
|
|
105
|
-
console.log(ui.bold(' MongoDB Atlas'));
|
|
106
|
-
const db = await ask(rl, 'Database name', defaults.db || 'myapp');
|
|
107
|
-
const collection = await ask(rl, 'Collection name', defaults.collection || 'documents');
|
|
108
|
-
const field = await ask(rl, 'Embedding field', defaults.field);
|
|
109
|
-
const index = await ask(rl, 'Vector index name', defaults.index);
|
|
110
|
-
|
|
111
|
-
// Dimensions
|
|
112
|
-
const modelInfo = MODEL_CATALOG.find(m => m.name === model);
|
|
113
|
-
const defaultDims = modelInfo && modelInfo.dimensions.includes('1024') ? '1024' : '512';
|
|
114
|
-
const dimensions = parseInt(await ask(rl, 'Dimensions', defaultDims), 10) || parseInt(defaultDims, 10);
|
|
56
|
+
if (cancelled) {
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
|
115
59
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
60
|
+
// Build config from answers
|
|
61
|
+
const config = {
|
|
62
|
+
model: answers.model || defaults.model,
|
|
63
|
+
db: answers.db || defaults.db,
|
|
64
|
+
collection: answers.collection || defaults.collection,
|
|
65
|
+
field: answers.field || defaults.field,
|
|
66
|
+
inputType: 'document',
|
|
67
|
+
dimensions: parseInt(answers.dimensions, 10) || defaults.dimensions,
|
|
68
|
+
index: answers.index || defaults.index,
|
|
69
|
+
chunk: {
|
|
70
|
+
strategy: answers.chunkStrategy || defaults.chunk.strategy,
|
|
71
|
+
size: parseInt(answers.chunkSize, 10) || defaults.chunk.size,
|
|
72
|
+
overlap: parseInt(answers.chunkOverlap, 10) || defaults.chunk.overlap,
|
|
73
|
+
},
|
|
74
|
+
};
|
|
122
75
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
db,
|
|
126
|
-
collection,
|
|
127
|
-
field,
|
|
128
|
-
inputType: 'document',
|
|
129
|
-
dimensions,
|
|
130
|
-
index,
|
|
131
|
-
chunk: {
|
|
132
|
-
strategy,
|
|
133
|
-
size: chunkSize,
|
|
134
|
-
overlap: chunkOverlap,
|
|
135
|
-
},
|
|
136
|
-
};
|
|
76
|
+
const filePath = saveProject(config);
|
|
77
|
+
const relPath = path.relative(process.cwd(), filePath);
|
|
137
78
|
|
|
138
|
-
|
|
139
|
-
console.log('');
|
|
140
|
-
console.log(ui.success(`Created ${path.relative(process.cwd(), filePath)}`));
|
|
79
|
+
if (!opts.quiet) {
|
|
141
80
|
console.log('');
|
|
142
81
|
console.log(ui.dim(' Next steps:'));
|
|
143
82
|
console.log(ui.dim(' vai chunk ./docs/ # Chunk your documents'));
|
|
144
|
-
console.log(ui.dim(' vai pipeline ./docs/ # Chunk → embed → store
|
|
83
|
+
console.log(ui.dim(' vai pipeline ./docs/ # Chunk → embed → store'));
|
|
145
84
|
console.log(ui.dim(' vai search --query "..." # Search your collection'));
|
|
146
85
|
console.log('');
|
|
147
|
-
} finally {
|
|
148
|
-
rl.close();
|
|
149
86
|
}
|
|
150
87
|
});
|
|
151
88
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register the mcp-server command (aliased as mcp).
|
|
5
|
+
* @param {import('commander').Command} program
|
|
6
|
+
*/
|
|
7
|
+
function registerMcpServer(program) {
|
|
8
|
+
const cmd = program
|
|
9
|
+
.command('mcp-server')
|
|
10
|
+
.alias('mcp')
|
|
11
|
+
.description('Start the MCP (Model Context Protocol) server — expose vai tools to AI agents')
|
|
12
|
+
.option('--transport <mode>', 'Transport mode: stdio or http', 'stdio')
|
|
13
|
+
.option('--port <number>', 'HTTP port (http transport only)', (v) => parseInt(v, 10), 3100)
|
|
14
|
+
.option('--host <address>', 'Bind address (http transport only)', '127.0.0.1')
|
|
15
|
+
.option('--db <name>', 'Default MongoDB database for tools')
|
|
16
|
+
.option('--collection <name>', 'Default collection for tools')
|
|
17
|
+
.option('--verbose', 'Enable debug logging to stderr')
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
if (opts.verbose) {
|
|
20
|
+
process.env.VAI_MCP_VERBOSE = '1';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Set default db/collection if provided via CLI
|
|
24
|
+
if (opts.db) process.env.VAI_DEFAULT_DB = opts.db;
|
|
25
|
+
if (opts.collection) process.env.VAI_DEFAULT_COLLECTION = opts.collection;
|
|
26
|
+
|
|
27
|
+
const { runStdioServer, runHttpServer } = require('../mcp/server');
|
|
28
|
+
|
|
29
|
+
if (opts.transport === 'http') {
|
|
30
|
+
await runHttpServer({ port: opts.port, host: opts.host });
|
|
31
|
+
} else if (opts.transport === 'stdio') {
|
|
32
|
+
await runStdioServer();
|
|
33
|
+
} else {
|
|
34
|
+
console.error(`Unknown transport: ${opts.transport}. Use "stdio" or "http".`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Subcommand: generate-key
|
|
40
|
+
cmd
|
|
41
|
+
.command('generate-key')
|
|
42
|
+
.description('Generate a new API key for remote MCP server authentication')
|
|
43
|
+
.action(() => {
|
|
44
|
+
const { generateKey } = require('../mcp/server');
|
|
45
|
+
generateKey();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { registerMcpServer };
|
package/src/commands/ping.js
CHANGED
|
@@ -190,6 +190,58 @@ function registerPing(program) {
|
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
// ── LLM provider ping (optional) ──
|
|
194
|
+
const { createLLMProvider, resolveLLMConfig } = require('../lib/llm');
|
|
195
|
+
const llmConfig = resolveLLMConfig();
|
|
196
|
+
if (llmConfig.provider) {
|
|
197
|
+
const llmStart = Date.now();
|
|
198
|
+
let llmSpin;
|
|
199
|
+
if (useSpinner) {
|
|
200
|
+
llmSpin = ui.spinner(`Testing LLM provider (${llmConfig.provider})...`);
|
|
201
|
+
llmSpin.start();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const llm = createLLMProvider();
|
|
206
|
+
const pingResult = await llm.ping();
|
|
207
|
+
const llmElapsed = Date.now() - llmStart;
|
|
208
|
+
|
|
209
|
+
results.llm = { ok: pingResult.ok, elapsed: llmElapsed, provider: llmConfig.provider, model: pingResult.model };
|
|
210
|
+
if (pingResult.error) results.llm.error = pingResult.error;
|
|
211
|
+
|
|
212
|
+
if (llmSpin) llmSpin.stop();
|
|
213
|
+
|
|
214
|
+
if (!opts.json && !opts.quiet) {
|
|
215
|
+
console.log('');
|
|
216
|
+
if (pingResult.ok) {
|
|
217
|
+
console.log(ui.success(`LLM Provider connected ${ui.dim('(' + llmElapsed + 'ms)')}`));
|
|
218
|
+
console.log(ui.label('Provider', llmConfig.provider));
|
|
219
|
+
console.log(ui.label('Model', pingResult.model));
|
|
220
|
+
} else {
|
|
221
|
+
console.log(ui.error(`LLM Provider failed: ${pingResult.error}`));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if (llmSpin) llmSpin.stop();
|
|
226
|
+
const llmElapsed = Date.now() - llmStart;
|
|
227
|
+
results.llm = { ok: false, elapsed: llmElapsed, provider: llmConfig.provider, error: err.message };
|
|
228
|
+
if (!opts.json && !opts.quiet) {
|
|
229
|
+
console.log('');
|
|
230
|
+
console.log(ui.error(`LLM Provider error: ${err.message}`));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Chat readiness summary ──
|
|
236
|
+
if (!opts.json && !opts.quiet && llmConfig.provider) {
|
|
237
|
+
console.log('');
|
|
238
|
+
if (results.voyage?.ok && results.llm?.ok) {
|
|
239
|
+
console.log(ui.success('Chat is ready. Run: vai chat'));
|
|
240
|
+
} else if (!results.llm?.ok) {
|
|
241
|
+
console.log(ui.warn('Chat requires a working LLM provider. Check your configuration.'));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
193
245
|
// Emit JSON at the end with all results
|
|
194
246
|
if (opts.json) {
|
|
195
247
|
console.log(JSON.stringify({ ok: true, ...results }, null, 2));
|
package/src/commands/pipeline.js
CHANGED
|
@@ -62,6 +62,7 @@ function registerPipeline(program) {
|
|
|
62
62
|
.option('--ignore <dirs>', 'Directory names to skip', 'node_modules,.git,__pycache__')
|
|
63
63
|
.option('--create-index', 'Auto-create vector search index if it doesn\'t exist')
|
|
64
64
|
.option('--dry-run', 'Show what would happen without executing')
|
|
65
|
+
.option('--estimate', 'Show estimated tokens and cost without executing')
|
|
65
66
|
.option('--json', 'Machine-readable JSON output')
|
|
66
67
|
.option('-q, --quiet', 'Suppress non-essential output')
|
|
67
68
|
.action(async (input, opts) => {
|
|
@@ -75,7 +76,7 @@ function registerPipeline(program) {
|
|
|
75
76
|
const collection = opts.collection || proj.collection;
|
|
76
77
|
const field = opts.field || proj.field || 'embedding';
|
|
77
78
|
const index = opts.index || proj.index || 'vector_index';
|
|
78
|
-
|
|
79
|
+
let model = opts.model || proj.model || getDefaultModel();
|
|
79
80
|
const dimensions = opts.dimensions || proj.dimensions;
|
|
80
81
|
const strategy = opts.strategy || projChunk.strategy || 'recursive';
|
|
81
82
|
const chunkSize = opts.chunkSize || projChunk.size || 512;
|
|
@@ -175,22 +176,35 @@ function registerPipeline(program) {
|
|
|
175
176
|
|
|
176
177
|
// Dry run — stop here
|
|
177
178
|
if (opts.dryRun) {
|
|
179
|
+
const { estimateCost, formatCostEstimate } = require('../lib/cost');
|
|
180
|
+
const est = estimateCost(totalTokens, model);
|
|
178
181
|
if (opts.json) {
|
|
179
182
|
console.log(JSON.stringify({
|
|
180
183
|
dryRun: true,
|
|
181
184
|
files: files.length,
|
|
182
185
|
chunks: allChunks.length,
|
|
183
186
|
estimatedTokens: totalTokens,
|
|
187
|
+
estimatedCost: est.cost,
|
|
188
|
+
pricePerMToken: est.pricePerMToken,
|
|
184
189
|
strategy, chunkSize, overlap, model, db, collection, field,
|
|
185
190
|
}, null, 2));
|
|
186
191
|
} else {
|
|
187
192
|
console.log(ui.success(`Dry run complete: ${fmtNum(allChunks.length)} chunks from ${files.length} files.`));
|
|
188
|
-
|
|
189
|
-
console.log(
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(formatCostEstimate(est));
|
|
195
|
+
console.log('');
|
|
190
196
|
}
|
|
191
197
|
return;
|
|
192
198
|
}
|
|
193
199
|
|
|
200
|
+
// Estimate — show comparison table, let user confirm or switch model, then continue
|
|
201
|
+
if (opts.estimate) {
|
|
202
|
+
const { confirmOrSwitchModel } = require('../lib/cost');
|
|
203
|
+
const chosenModel = await confirmOrSwitchModel(totalTokens, model, { json: opts.json });
|
|
204
|
+
if (!chosenModel) return; // cancelled
|
|
205
|
+
model = chosenModel;
|
|
206
|
+
}
|
|
207
|
+
|
|
194
208
|
// Step 3: Embed in batches
|
|
195
209
|
if (verbose) console.log(ui.bold('Step 2/3 — Embedding'));
|
|
196
210
|
|
|
@@ -61,6 +61,9 @@ function createPlaygroundServer() {
|
|
|
61
61
|
|
|
62
62
|
const htmlPath = path.join(__dirname, '..', 'playground', 'index.html');
|
|
63
63
|
|
|
64
|
+
// Chat history — scoped to the server lifetime (in-memory, no persistence)
|
|
65
|
+
let _chatHistory = null;
|
|
66
|
+
|
|
64
67
|
const server = http.createServer(async (req, res) => {
|
|
65
68
|
// CORS headers for local dev
|
|
66
69
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -229,6 +232,40 @@ function createPlaygroundServer() {
|
|
|
229
232
|
return;
|
|
230
233
|
}
|
|
231
234
|
|
|
235
|
+
// API: Chat config (GET)
|
|
236
|
+
if (req.method === 'GET' && req.url === '/api/chat/config') {
|
|
237
|
+
const { resolveLLMConfig } = require('../lib/llm');
|
|
238
|
+
const { loadProject } = require('../lib/project');
|
|
239
|
+
const llmConfig = resolveLLMConfig();
|
|
240
|
+
const { config: proj } = loadProject();
|
|
241
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
242
|
+
res.end(JSON.stringify({
|
|
243
|
+
provider: llmConfig.provider || null,
|
|
244
|
+
model: llmConfig.model || null,
|
|
245
|
+
hasLLMKey: !!llmConfig.apiKey || llmConfig.provider === 'ollama',
|
|
246
|
+
db: proj.db || null,
|
|
247
|
+
collection: proj.collection || null,
|
|
248
|
+
chat: proj.chat || {},
|
|
249
|
+
}));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// API: Chat models — list available models for a provider
|
|
254
|
+
if (req.method === 'GET' && req.url?.startsWith('/api/chat/models')) {
|
|
255
|
+
const url = new URL(req.url, 'http://localhost');
|
|
256
|
+
const provider = url.searchParams.get('provider');
|
|
257
|
+
if (!provider) {
|
|
258
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
259
|
+
res.end(JSON.stringify({ error: 'provider query param required' }));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const { listModels } = require('../lib/llm');
|
|
263
|
+
const models = await listModels(provider);
|
|
264
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
265
|
+
res.end(JSON.stringify({ provider, models }));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
232
269
|
// API: Config
|
|
233
270
|
if (req.method === 'GET' && req.url === '/api/config') {
|
|
234
271
|
const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
|
|
@@ -240,6 +277,74 @@ function createPlaygroundServer() {
|
|
|
240
277
|
return;
|
|
241
278
|
}
|
|
242
279
|
|
|
280
|
+
// API: Settings origins — where each config value comes from
|
|
281
|
+
if (req.method === 'GET' && req.url === '/api/settings/origins') {
|
|
282
|
+
const { resolveLLMConfig } = require('../lib/llm');
|
|
283
|
+
const { loadProject } = require('../lib/project');
|
|
284
|
+
const { config: proj } = loadProject();
|
|
285
|
+
const chatConf = proj.chat || {};
|
|
286
|
+
|
|
287
|
+
function resolveOrigin(envVar, configKey, projectValue) {
|
|
288
|
+
if (envVar && process.env[envVar]) return 'env';
|
|
289
|
+
if (configKey && getConfigValue(configKey)) return 'config';
|
|
290
|
+
if (projectValue) return 'project';
|
|
291
|
+
return 'default';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const origins = {
|
|
295
|
+
apiKey: resolveOrigin('VOYAGE_API_KEY', 'apiKey'),
|
|
296
|
+
apiBase: resolveOrigin('VOYAGE_API_BASE', 'baseUrl'),
|
|
297
|
+
provider: resolveOrigin('VAI_LLM_PROVIDER', 'llmProvider', chatConf.provider),
|
|
298
|
+
model: resolveOrigin('VAI_LLM_MODEL', 'llmModel', chatConf.model),
|
|
299
|
+
llmApiKey: resolveOrigin('VAI_LLM_API_KEY', 'llmApiKey'),
|
|
300
|
+
db: proj.db ? 'project' : 'default',
|
|
301
|
+
collection: proj.collection ? 'project' : 'default',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
305
|
+
res.end(JSON.stringify(origins));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// API: Save chat config (POST) — persists to .vai.json
|
|
310
|
+
// Placed before generic POST handler so it doesn't require Voyage API key
|
|
311
|
+
if (req.method === 'POST' && req.url === '/api/chat/config') {
|
|
312
|
+
const { loadProject, saveProject } = require('../lib/project');
|
|
313
|
+
const body = await readBody(req);
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
parsed = JSON.parse(body);
|
|
317
|
+
} catch {
|
|
318
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
319
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const { config: proj, filePath } = loadProject();
|
|
324
|
+
|
|
325
|
+
// Update top-level project fields
|
|
326
|
+
if (parsed.db !== undefined) proj.db = parsed.db;
|
|
327
|
+
if (parsed.collection !== undefined) proj.collection = parsed.collection;
|
|
328
|
+
|
|
329
|
+
// Update chat-specific settings
|
|
330
|
+
proj.chat = proj.chat || {};
|
|
331
|
+
if (parsed.provider !== undefined) proj.chat.provider = parsed.provider;
|
|
332
|
+
if (parsed.model !== undefined) proj.chat.model = parsed.model;
|
|
333
|
+
if (parsed.maxDocs !== undefined) proj.chat.maxContextDocs = parsed.maxDocs;
|
|
334
|
+
if (parsed.rerank !== undefined) proj.chat.rerank = parsed.rerank;
|
|
335
|
+
if (parsed.systemPrompt !== undefined) proj.chat.systemPrompt = parsed.systemPrompt;
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
saveProject(proj, filePath || undefined);
|
|
339
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
340
|
+
res.end(JSON.stringify({ ok: true }));
|
|
341
|
+
} catch (err) {
|
|
342
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
343
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
243
348
|
// Parse JSON body for POST routes
|
|
244
349
|
if (req.method === 'POST') {
|
|
245
350
|
// Check for API key before processing any API calls
|
|
@@ -263,6 +368,87 @@ function createPlaygroundServer() {
|
|
|
263
368
|
return;
|
|
264
369
|
}
|
|
265
370
|
|
|
371
|
+
// API: Chat message (streaming SSE)
|
|
372
|
+
if (req.url === '/api/chat/message') {
|
|
373
|
+
const { query, db, collection, provider, model, maxDocs, rerank, systemPrompt } = parsed;
|
|
374
|
+
if (!query) {
|
|
375
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
376
|
+
res.end(JSON.stringify({ error: 'query is required' }));
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
if (!db || !collection) {
|
|
380
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
381
|
+
res.end(JSON.stringify({ error: 'db and collection are required' }));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const { createLLMProvider } = require('../lib/llm');
|
|
386
|
+
const { chatTurn } = require('../lib/chat');
|
|
387
|
+
const { ChatHistory } = require('../lib/history');
|
|
388
|
+
|
|
389
|
+
let llm;
|
|
390
|
+
try {
|
|
391
|
+
llm = createLLMProvider({
|
|
392
|
+
llmProvider: provider || undefined,
|
|
393
|
+
llmModel: model || undefined,
|
|
394
|
+
});
|
|
395
|
+
} catch (err) {
|
|
396
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
397
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!llm) {
|
|
402
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
403
|
+
res.end(JSON.stringify({ error: 'No LLM provider configured. Use vai config set llm-provider <name>' }));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Use in-memory history for playground (no session persistence)
|
|
408
|
+
if (!_chatHistory) _chatHistory = new ChatHistory({ maxTurns: 20 });
|
|
409
|
+
const history = _chatHistory;
|
|
410
|
+
|
|
411
|
+
// Stream response as SSE
|
|
412
|
+
res.writeHead(200, {
|
|
413
|
+
'Content-Type': 'text/event-stream',
|
|
414
|
+
'Cache-Control': 'no-cache',
|
|
415
|
+
'Connection': 'keep-alive',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
for await (const event of chatTurn({
|
|
420
|
+
query, db, collection, llm, history,
|
|
421
|
+
opts: {
|
|
422
|
+
maxDocs: maxDocs || 5,
|
|
423
|
+
rerank: rerank !== false,
|
|
424
|
+
stream: true,
|
|
425
|
+
systemPrompt,
|
|
426
|
+
},
|
|
427
|
+
})) {
|
|
428
|
+
if (event.type === 'retrieval') {
|
|
429
|
+
res.write(`event: retrieval\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
430
|
+
} else if (event.type === 'chunk') {
|
|
431
|
+
res.write(`event: chunk\ndata: ${JSON.stringify({ text: event.data })}\n\n`);
|
|
432
|
+
} else if (event.type === 'done') {
|
|
433
|
+
res.write(`event: done\ndata: ${JSON.stringify(event.data)}\n\n`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
res.write(`event: error\ndata: ${JSON.stringify({ error: err.message })}\n\n`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
res.end();
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// API: Chat clear
|
|
445
|
+
if (req.url === '/api/chat/clear') {
|
|
446
|
+
if (_chatHistory) _chatHistory.clear();
|
|
447
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
448
|
+
res.end(JSON.stringify({ ok: true }));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
266
452
|
// API: Embed
|
|
267
453
|
if (req.url === '/api/embed') {
|
|
268
454
|
const { texts, model, inputType, dimensions, output_dtype } = parsed;
|
package/src/commands/purge.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
|
|
5
|
+
let p;
|
|
6
|
+
function clack() { if (!p) p = require('@clack/prompts'); return p; }
|
|
6
7
|
const { loadProject } = require('../lib/project');
|
|
7
8
|
const { connect, close } = require('../lib/mongo');
|
|
8
9
|
const ui = require('../lib/ui');
|
|
@@ -95,6 +96,7 @@ function formatSample(docs, limit = 5) {
|
|
|
95
96
|
* Execute the purge command.
|
|
96
97
|
*/
|
|
97
98
|
async function purge(options = {}) {
|
|
99
|
+
clack(); // lazy-load @clack/prompts
|
|
98
100
|
const quiet = options.quiet || options.json;
|
|
99
101
|
|
|
100
102
|
// Load project config
|
package/src/commands/refresh.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
let p;
|
|
4
|
+
function clack() { if (!p) p = require('@clack/prompts'); return p; }
|
|
4
5
|
const { loadProject, saveProject } = require('../lib/project');
|
|
5
6
|
const { connect, close } = require('../lib/mongo');
|
|
6
7
|
const { generateEmbeddings } = require('../lib/api');
|
|
@@ -53,6 +54,7 @@ function rechunkDocument(doc, options) {
|
|
|
53
54
|
* Execute the refresh command.
|
|
54
55
|
*/
|
|
55
56
|
async function refresh(options = {}) {
|
|
57
|
+
clack(); // lazy-load @clack/prompts
|
|
56
58
|
const quiet = options.quiet || options.json;
|
|
57
59
|
|
|
58
60
|
// Load project config
|
package/src/commands/rerank.js
CHANGED
|
@@ -21,6 +21,7 @@ function registerRerank(program) {
|
|
|
21
21
|
.option('--truncation', 'Enable truncation for long inputs')
|
|
22
22
|
.option('--no-truncation', 'Disable truncation')
|
|
23
23
|
.option('--return-documents', 'Return document text in results')
|
|
24
|
+
.option('--estimate', 'Show estimated tokens and cost without calling the API')
|
|
24
25
|
.option('--json', 'Machine-readable JSON output')
|
|
25
26
|
.option('-q, --quiet', 'Suppress non-essential output')
|
|
26
27
|
.action(async (opts) => {
|
|
@@ -71,6 +72,15 @@ function registerRerank(program) {
|
|
|
71
72
|
process.exit(1);
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
// --estimate: show cost comparison, optionally switch model
|
|
76
|
+
if (opts.estimate) {
|
|
77
|
+
const { estimateTokens, confirmOrSwitchModel } = require('../lib/cost');
|
|
78
|
+
const tokens = estimateTokens(opts.query) + documents.reduce((s, d) => s + estimateTokens(d), 0);
|
|
79
|
+
const chosenModel = await confirmOrSwitchModel(tokens, opts.model, { json: opts.json });
|
|
80
|
+
if (!chosenModel) return; // cancelled
|
|
81
|
+
opts.model = chosenModel;
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
const body = {
|
|
75
85
|
query: opts.query,
|
|
76
86
|
documents,
|
package/src/commands/scaffold.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const p = require('@clack/prompts');
|
|
6
5
|
const { loadProject } = require('../lib/project');
|
|
7
6
|
const { renderTemplate, buildContext, listTemplates } = require('../lib/codegen');
|
|
8
7
|
const { PROJECT_STRUCTURE } = require('../lib/scaffold-structure');
|
|
@@ -193,7 +192,7 @@ function registerScaffold(program) {
|
|
|
193
192
|
structure.startCommand,
|
|
194
193
|
];
|
|
195
194
|
|
|
196
|
-
|
|
195
|
+
require('@clack/prompts').note(steps.join('\n'), 'Next steps');
|
|
197
196
|
|
|
198
197
|
console.log('');
|
|
199
198
|
console.log(ui.dim('Configuration:'));
|