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.
@@ -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
- const filePath = saveProject(defaults);
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
- console.log('');
86
- console.log(ui.bold(' 🚀 Initialize Voyage AI Project'));
87
- console.log(ui.dim(' Creates .vai.json in the current directory.'));
88
- console.log(ui.dim(' Press Enter to accept defaults.'));
89
- console.log('');
90
-
91
- const rl = readline.createInterface({
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
- try {
97
- // Embedding model
98
- const embeddingModels = MODEL_CATALOG
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
- // Chunking
117
- console.log('');
118
- console.log(ui.bold(' Chunking'));
119
- const strategy = await askChoice(rl, 'Chunk strategy', STRATEGIES, defaults.chunk.strategy);
120
- const chunkSize = parseInt(await ask(rl, 'Chunk size (chars)', String(defaults.chunk.size)), 10);
121
- const chunkOverlap = parseInt(await ask(rl, 'Chunk overlap (chars)', String(defaults.chunk.overlap)), 10);
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
- const config = {
124
- model,
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
- const filePath = saveProject(config);
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 (coming soon)'));
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 };
@@ -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));
@@ -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
- const model = opts.model || proj.model || getDefaultModel();
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
- const cost = (totalTokens / 1e6) * 0.12;
189
- console.log(ui.dim(` Estimated embedding cost: ~$${cost.toFixed(4)} with ${model}`));
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;
@@ -2,7 +2,8 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const p = require('@clack/prompts');
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
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const p = require('@clack/prompts');
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
@@ -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,
@@ -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
- p.note(steps.join('\n'), 'Next steps');
195
+ require('@clack/prompts').note(steps.join('\n'), 'Next steps');
197
196
 
198
197
  console.log('');
199
198
  console.log(ui.dim('Configuration:'));