voyageai-cli 1.1.0 → 1.2.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.
@@ -0,0 +1,169 @@
1
+ 'use strict';
2
+
3
+ const { API_BASE, requireApiKey } = require('../lib/api');
4
+ const ui = require('../lib/ui');
5
+
6
+ /**
7
+ * Register the ping command on a Commander program.
8
+ * @param {import('commander').Command} program
9
+ */
10
+ function registerPing(program) {
11
+ program
12
+ .command('ping')
13
+ .description('Test connectivity to Voyage AI API (and optionally MongoDB)')
14
+ .option('--json', 'Machine-readable JSON output')
15
+ .option('-q, --quiet', 'Suppress non-essential output')
16
+ .action(async (opts) => {
17
+ const results = {};
18
+
19
+ // ── Voyage AI ping ──
20
+ let apiKey;
21
+ try {
22
+ apiKey = requireApiKey();
23
+ } catch {
24
+ // requireApiKey calls process.exit, but just in case
25
+ process.exit(1);
26
+ }
27
+
28
+ const useColor = !opts.json;
29
+ const useSpinner = useColor && !opts.quiet;
30
+
31
+ const model = 'voyage-4-lite';
32
+ const startTime = Date.now();
33
+
34
+ let spin;
35
+ if (useSpinner) {
36
+ spin = ui.spinner('Testing Voyage AI connection...');
37
+ spin.start();
38
+ }
39
+
40
+ try {
41
+ const response = await fetch(`${API_BASE}/embeddings`, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ 'Authorization': `Bearer ${apiKey}`,
46
+ },
47
+ body: JSON.stringify({
48
+ input: ['ping'],
49
+ model,
50
+ }),
51
+ });
52
+
53
+ const elapsed = Date.now() - startTime;
54
+
55
+ if (response.status === 401 || response.status === 403) {
56
+ if (spin) spin.stop();
57
+ results.voyage = { ok: false, error: 'auth', elapsed };
58
+ if (opts.json) {
59
+ console.log(JSON.stringify({ ok: false, error: 'Authentication failed', elapsed }));
60
+ } else {
61
+ console.error(ui.error(`Authentication failed (${response.status})`));
62
+ console.error('');
63
+ console.error('Your API key may be invalid or expired.');
64
+ console.error('Get a new key: MongoDB Atlas → AI Models → Create model API key');
65
+ console.error('Then: export VOYAGE_API_KEY="your-new-key"');
66
+ }
67
+ process.exit(1);
68
+ }
69
+
70
+ if (!response.ok) {
71
+ if (spin) spin.stop();
72
+ const body = await response.text();
73
+ results.voyage = { ok: false, error: `HTTP ${response.status}`, elapsed };
74
+ if (opts.json) {
75
+ console.log(JSON.stringify({ ok: false, error: `API error (${response.status})`, detail: body, elapsed }));
76
+ } else {
77
+ console.error(ui.error(`API error (${response.status}): ${body}`));
78
+ }
79
+ process.exit(1);
80
+ }
81
+
82
+ const data = await response.json();
83
+ const dims = data.data && data.data[0] ? data.data[0].embedding.length : 'unknown';
84
+ const tokens = data.usage ? data.usage.total_tokens : 'unknown';
85
+
86
+ results.voyage = { ok: true, elapsed, model, dimensions: dims, tokens, endpoint: API_BASE };
87
+
88
+ if (spin) spin.stop();
89
+
90
+ if (opts.json) {
91
+ // JSON output is emitted at the end after MongoDB check
92
+ } else if (opts.quiet) {
93
+ console.log(`ok ${elapsed}ms`);
94
+ } else {
95
+ console.log(ui.success(`Connected to Voyage AI API ${ui.dim('(' + elapsed + 'ms)')}`));
96
+ console.log(ui.label('Endpoint', API_BASE));
97
+ console.log(ui.label('Model', model));
98
+ console.log(ui.label('Dimensions', String(dims)));
99
+ console.log(ui.label('Tokens', String(tokens)));
100
+ }
101
+ } catch (err) {
102
+ if (spin) spin.stop();
103
+ const elapsed = Date.now() - startTime;
104
+ results.voyage = { ok: false, error: 'network', elapsed };
105
+ if (opts.json) {
106
+ console.log(JSON.stringify({ ok: false, error: 'Network error', detail: err.message, elapsed }));
107
+ } else {
108
+ console.error(ui.error(`Connection failed: ${err.message}`));
109
+ console.error('');
110
+ console.error('Check your internet connection and try again.');
111
+ }
112
+ process.exit(1);
113
+ }
114
+
115
+ // ── MongoDB ping (optional) ──
116
+ const { getConfigValue } = require('../lib/config');
117
+ const mongoUri = process.env.MONGODB_URI || getConfigValue('mongodbUri');
118
+ if (mongoUri) {
119
+ const mongoStart = Date.now();
120
+ let mongoSpin;
121
+ if (useSpinner) {
122
+ mongoSpin = ui.spinner('Testing MongoDB connection...');
123
+ mongoSpin.start();
124
+ }
125
+
126
+ try {
127
+ const { MongoClient } = require('mongodb');
128
+ const client = new MongoClient(mongoUri);
129
+ await client.connect();
130
+ await client.db('admin').command({ ping: 1 });
131
+ const mongoElapsed = Date.now() - mongoStart;
132
+
133
+ // Extract cluster hostname from URI
134
+ let cluster = 'unknown';
135
+ try {
136
+ const match = mongoUri.match(/@([^/?]+)/);
137
+ if (match) cluster = match[1];
138
+ } catch { /* ignore */ }
139
+
140
+ results.mongodb = { ok: true, elapsed: mongoElapsed, cluster };
141
+
142
+ if (mongoSpin) mongoSpin.stop();
143
+
144
+ if (!opts.json && !opts.quiet) {
145
+ console.log('');
146
+ console.log(ui.success(`Connected to MongoDB Atlas ${ui.dim('(' + mongoElapsed + 'ms)')}`));
147
+ console.log(ui.label('Cluster', cluster));
148
+ }
149
+
150
+ await client.close();
151
+ } catch (err) {
152
+ if (mongoSpin) mongoSpin.stop();
153
+ const mongoElapsed = Date.now() - mongoStart;
154
+ results.mongodb = { ok: false, elapsed: mongoElapsed, error: err.message };
155
+ if (!opts.json && !opts.quiet) {
156
+ console.log('');
157
+ console.log(ui.error(`MongoDB connection failed ${ui.dim('(' + mongoElapsed + 'ms)')}: ${err.message}`));
158
+ }
159
+ }
160
+ }
161
+
162
+ // Emit JSON at the end with all results
163
+ if (opts.json) {
164
+ console.log(JSON.stringify({ ok: true, ...results }, null, 2));
165
+ }
166
+ });
167
+ }
168
+
169
+ module.exports = { registerPing };
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const { DEFAULT_RERANK_MODEL } = require('../lib/catalog');
5
5
  const { apiRequest } = require('../lib/api');
6
+ const ui = require('../lib/ui');
6
7
 
7
8
  /**
8
9
  * Register the rerank command on a Commander program.
@@ -63,7 +64,7 @@ function registerRerank(program) {
63
64
  }
64
65
 
65
66
  if (!documents || documents.length === 0) {
66
- console.error('Error: No documents provided. Use --documents, --documents-file, or pipe via stdin.');
67
+ console.error(ui.error('No documents provided. Use --documents, --documents-file, or pipe via stdin.'));
67
68
  process.exit(1);
68
69
  }
69
70
 
@@ -76,19 +77,29 @@ function registerRerank(program) {
76
77
  body.top_k = opts.topK;
77
78
  }
78
79
 
80
+ const useColor = !opts.json;
81
+ const useSpinner = useColor && !opts.quiet;
82
+ let spin;
83
+ if (useSpinner) {
84
+ spin = ui.spinner('Reranking documents...');
85
+ spin.start();
86
+ }
87
+
79
88
  const result = await apiRequest('/rerank', body);
80
89
 
90
+ if (spin) spin.stop();
91
+
81
92
  if (opts.json) {
82
93
  console.log(JSON.stringify(result, null, 2));
83
94
  return;
84
95
  }
85
96
 
86
97
  if (!opts.quiet) {
87
- console.log(`Model: ${result.model}`);
88
- console.log(`Query: "${opts.query}"`);
89
- console.log(`Results: ${result.data?.length || 0}`);
98
+ console.log(ui.label('Model', ui.cyan(result.model)));
99
+ console.log(ui.label('Query', ui.cyan(`"${opts.query}"`)));
100
+ console.log(ui.label('Results', String(result.data?.length || 0)));
90
101
  if (result.usage) {
91
- console.log(`Tokens: ${result.usage.total_tokens}`);
102
+ console.log(ui.label('Tokens', ui.dim(String(result.usage.total_tokens))));
92
103
  }
93
104
  console.log('');
94
105
  }
@@ -97,11 +108,14 @@ function registerRerank(program) {
97
108
  for (const item of result.data) {
98
109
  const docPreview = documents[item.index].substring(0, 80);
99
110
  const ellipsis = documents[item.index].length > 80 ? '...' : '';
100
- console.log(`[${item.index}] Score: ${item.relevance_score.toFixed(6)} "${docPreview}${ellipsis}"`);
111
+ console.log(`${ui.dim('[' + item.index + ']')} Score: ${ui.score(item.relevance_score)} ${ui.dim('"' + docPreview + ellipsis + '"')}`);
101
112
  }
102
113
  }
114
+
115
+ console.log('');
116
+ console.log(ui.success('Reranking complete'));
103
117
  } catch (err) {
104
- console.error(`Error: ${err.message}`);
118
+ console.error(ui.error(err.message));
105
119
  process.exit(1);
106
120
  }
107
121
  });
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { DEFAULT_EMBED_MODEL } = require('../lib/catalog');
3
+ const { getDefaultModel } = require('../lib/catalog');
4
4
  const { generateEmbeddings } = require('../lib/api');
5
5
  const { getMongoCollection } = require('../lib/mongo');
6
+ const ui = require('../lib/ui');
6
7
 
7
8
  /**
8
9
  * Register the search command on a Commander program.
@@ -17,7 +18,7 @@ function registerSearch(program) {
17
18
  .requiredOption('--collection <name>', 'Collection name')
18
19
  .requiredOption('--index <name>', 'Vector search index name')
19
20
  .requiredOption('--field <name>', 'Embedding field name')
20
- .option('-m, --model <model>', 'Embedding model', DEFAULT_EMBED_MODEL)
21
+ .option('-m, --model <model>', 'Embedding model', getDefaultModel())
21
22
  .option('--input-type <type>', 'Input type for query embedding', 'query')
22
23
  .option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
23
24
  .option('-l, --limit <n>', 'Maximum results', (v) => parseInt(v, 10), 10)
@@ -29,6 +30,14 @@ function registerSearch(program) {
29
30
  .action(async (opts) => {
30
31
  let client;
31
32
  try {
33
+ const useColor = !opts.json;
34
+ const useSpinner = useColor && !opts.quiet;
35
+ let spin;
36
+ if (useSpinner) {
37
+ spin = ui.spinner('Searching...');
38
+ spin.start();
39
+ }
40
+
32
41
  const embedResult = await generateEmbeddings([opts.query], {
33
42
  model: opts.model,
34
43
  inputType: opts.inputType,
@@ -54,7 +63,8 @@ function registerSearch(program) {
54
63
  try {
55
64
  vectorSearchStage.filter = JSON.parse(opts.filter);
56
65
  } catch (e) {
57
- console.error('Error: Invalid filter JSON. Ensure it is valid JSON.');
66
+ if (spin) spin.stop();
67
+ console.error(ui.error('Invalid filter JSON. Ensure it is valid JSON.'));
58
68
  process.exit(1);
59
69
  }
60
70
  }
@@ -67,6 +77,8 @@ function registerSearch(program) {
67
77
 
68
78
  const results = await collection.aggregate(pipeline).toArray();
69
79
 
80
+ if (spin) spin.stop();
81
+
70
82
  const cleanResults = results.map(doc => {
71
83
  const clean = { ...doc };
72
84
  delete clean[opts.field];
@@ -79,28 +91,29 @@ function registerSearch(program) {
79
91
  }
80
92
 
81
93
  if (!opts.quiet) {
82
- console.log(`Query: "${opts.query}"`);
83
- console.log(`Results: ${cleanResults.length}`);
94
+ console.log(ui.label('Query', ui.cyan(`"${opts.query}"`)));
95
+ console.log(ui.label('Results', String(cleanResults.length)));
84
96
  console.log('');
85
97
  }
86
98
 
87
99
  if (cleanResults.length === 0) {
88
- console.log('No results found.');
100
+ console.log(ui.yellow('No results found.'));
89
101
  return;
90
102
  }
91
103
 
92
104
  for (let i = 0; i < cleanResults.length; i++) {
93
105
  const doc = cleanResults[i];
94
- const score = doc.score?.toFixed(6) || 'N/A';
95
- console.log(`── Result ${i + 1} (score: ${score}) ──`);
106
+ const scoreVal = doc.score;
107
+ const scoreStr = scoreVal != null ? ui.score(scoreVal) : 'N/A';
108
+ console.log(`── ${ui.bold('Result ' + (i + 1))} (score: ${scoreStr}) ──`);
96
109
  const textPreview = doc.text ? doc.text.substring(0, 200) : 'No text field';
97
110
  const ellipsis = doc.text && doc.text.length > 200 ? '...' : '';
98
111
  console.log(` ${textPreview}${ellipsis}`);
99
- console.log(` _id: ${doc._id}`);
112
+ console.log(` ${ui.dim('_id: ' + doc._id)}`);
100
113
  console.log('');
101
114
  }
102
115
  } catch (err) {
103
- console.error(`Error: ${err.message}`);
116
+ console.error(ui.error(err.message));
104
117
  process.exit(1);
105
118
  } finally {
106
119
  if (client) await client.close();
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
- const { DEFAULT_EMBED_MODEL } = require('../lib/catalog');
4
+ const { getDefaultModel } = require('../lib/catalog');
5
5
  const { generateEmbeddings } = require('../lib/api');
6
6
  const { resolveTextInput } = require('../lib/input');
7
7
  const { getMongoCollection } = require('../lib/mongo');
8
+ const ui = require('../lib/ui');
8
9
 
9
10
  /**
10
11
  * Register the store command on a Commander program.
@@ -19,7 +20,7 @@ function registerStore(program) {
19
20
  .requiredOption('--field <name>', 'Embedding field name')
20
21
  .option('--text <text>', 'Text to embed and store')
21
22
  .option('-f, --file <path>', 'File to embed and store (text file or .jsonl for batch mode)')
22
- .option('-m, --model <model>', 'Embedding model', DEFAULT_EMBED_MODEL)
23
+ .option('-m, --model <model>', 'Embedding model', getDefaultModel())
23
24
  .option('--input-type <type>', 'Input type: query or document', 'document')
24
25
  .option('-d, --dimensions <n>', 'Output dimensions', (v) => parseInt(v, 10))
25
26
  .option('--metadata <json>', 'Additional metadata as JSON')
@@ -37,6 +38,14 @@ function registerStore(program) {
37
38
  const texts = await resolveTextInput(opts.text, opts.file);
38
39
  const textContent = texts[0];
39
40
 
41
+ const useColor = !opts.json;
42
+ const useSpinner = useColor && !opts.quiet;
43
+ let spin;
44
+ if (useSpinner) {
45
+ spin = ui.spinner('Embedding and storing...');
46
+ spin.start();
47
+ }
48
+
40
49
  const embedResult = await generateEmbeddings([textContent], {
41
50
  model: opts.model,
42
51
  inputType: opts.inputType,
@@ -48,7 +57,7 @@ function registerStore(program) {
48
57
  const doc = {
49
58
  text: textContent,
50
59
  [opts.field]: embedding,
51
- model: opts.model || DEFAULT_EMBED_MODEL,
60
+ model: opts.model || getDefaultModel(),
52
61
  dimensions: embedding.length,
53
62
  createdAt: new Date(),
54
63
  };
@@ -58,7 +67,8 @@ function registerStore(program) {
58
67
  const meta = JSON.parse(opts.metadata);
59
68
  Object.assign(doc, meta);
60
69
  } catch (e) {
61
- console.error('Error: Invalid metadata JSON. Ensure it is valid JSON.');
70
+ if (spin) spin.stop();
71
+ console.error(ui.error('Invalid metadata JSON. Ensure it is valid JSON.'));
62
72
  process.exit(1);
63
73
  }
64
74
  }
@@ -67,6 +77,8 @@ function registerStore(program) {
67
77
  client = c;
68
78
  const result = await collection.insertOne(doc);
69
79
 
80
+ if (spin) spin.stop();
81
+
70
82
  if (opts.json) {
71
83
  console.log(JSON.stringify({
72
84
  insertedId: result.insertedId,
@@ -75,18 +87,18 @@ function registerStore(program) {
75
87
  tokens: embedResult.usage?.total_tokens,
76
88
  }, null, 2));
77
89
  } else if (!opts.quiet) {
78
- console.log(`✓ Stored document: ${result.insertedId}`);
79
- console.log(` Database: ${opts.db}`);
80
- console.log(` Collection: ${opts.collection}`);
81
- console.log(` Field: ${opts.field}`);
82
- console.log(` Dimensions: ${embedding.length}`);
83
- console.log(` Model: ${doc.model}`);
90
+ console.log(ui.success('Stored document: ' + ui.cyan(String(result.insertedId))));
91
+ console.log(ui.label('Database', opts.db));
92
+ console.log(ui.label('Collection', opts.collection));
93
+ console.log(ui.label('Field', opts.field));
94
+ console.log(ui.label('Dimensions', String(embedding.length)));
95
+ console.log(ui.label('Model', doc.model));
84
96
  if (embedResult.usage) {
85
- console.log(` Tokens: ${embedResult.usage.total_tokens}`);
97
+ console.log(ui.label('Tokens', String(embedResult.usage.total_tokens)));
86
98
  }
87
99
  }
88
100
  } catch (err) {
89
- console.error(`Error: ${err.message}`);
101
+ console.error(ui.error(err.message));
90
102
  process.exit(1);
91
103
  } finally {
92
104
  if (client) await client.close();
@@ -106,7 +118,7 @@ async function handleBatchStore(opts) {
106
118
  const lines = content.split('\n').filter(line => line.trim());
107
119
 
108
120
  if (lines.length === 0) {
109
- console.error('Error: JSONL file is empty.');
121
+ console.error(ui.error('JSONL file is empty.'));
110
122
  process.exit(1);
111
123
  }
112
124
 
@@ -114,21 +126,25 @@ async function handleBatchStore(opts) {
114
126
  try {
115
127
  return JSON.parse(line);
116
128
  } catch (e) {
117
- console.error(`Error: Invalid JSON on line ${i + 1}: ${e.message}`);
129
+ console.error(ui.error(`Invalid JSON on line ${i + 1}: ${e.message}`));
118
130
  process.exit(1);
119
131
  }
120
132
  });
121
133
 
122
134
  const texts = records.map(r => {
123
135
  if (!r.text) {
124
- console.error('Error: Each JSONL line must have a "text" field.');
136
+ console.error(ui.error('Each JSONL line must have a "text" field.'));
125
137
  process.exit(1);
126
138
  }
127
139
  return r.text;
128
140
  });
129
141
 
130
- if (!opts.quiet) {
131
- console.log(`Embedding ${texts.length} documents...`);
142
+ const useColor = !opts.json;
143
+ const useSpinner = useColor && !opts.quiet;
144
+ let spin;
145
+ if (useSpinner) {
146
+ spin = ui.spinner(`Embedding and storing ${texts.length} documents...`);
147
+ spin.start();
132
148
  }
133
149
 
134
150
  const embedResult = await generateEmbeddings(texts, {
@@ -142,7 +158,7 @@ async function handleBatchStore(opts) {
142
158
  const doc = {
143
159
  text: record.text,
144
160
  [opts.field]: embedding,
145
- model: opts.model || DEFAULT_EMBED_MODEL,
161
+ model: opts.model || getDefaultModel(),
146
162
  dimensions: embedding.length,
147
163
  createdAt: new Date(),
148
164
  };
@@ -156,27 +172,29 @@ async function handleBatchStore(opts) {
156
172
  client = c;
157
173
  const result = await collection.insertMany(docs);
158
174
 
175
+ if (spin) spin.stop();
176
+
159
177
  if (opts.json) {
160
178
  console.log(JSON.stringify({
161
179
  insertedCount: result.insertedCount,
162
180
  insertedIds: result.insertedIds,
163
181
  dimensions: docs[0]?.dimensions,
164
- model: opts.model || DEFAULT_EMBED_MODEL,
182
+ model: opts.model || getDefaultModel(),
165
183
  tokens: embedResult.usage?.total_tokens,
166
184
  }, null, 2));
167
185
  } else if (!opts.quiet) {
168
- console.log(`✓ Stored ${result.insertedCount} documents`);
169
- console.log(` Database: ${opts.db}`);
170
- console.log(` Collection: ${opts.collection}`);
171
- console.log(` Field: ${opts.field}`);
172
- console.log(` Dimensions: ${docs[0]?.dimensions}`);
173
- console.log(` Model: ${opts.model || DEFAULT_EMBED_MODEL}`);
186
+ console.log(ui.success(`Stored ${result.insertedCount} documents`));
187
+ console.log(ui.label('Database', opts.db));
188
+ console.log(ui.label('Collection', opts.collection));
189
+ console.log(ui.label('Field', opts.field));
190
+ console.log(ui.label('Dimensions', String(docs[0]?.dimensions)));
191
+ console.log(ui.label('Model', opts.model || getDefaultModel()));
174
192
  if (embedResult.usage) {
175
- console.log(` Tokens: ${embedResult.usage.total_tokens}`);
193
+ console.log(ui.label('Tokens', String(embedResult.usage.total_tokens)));
176
194
  }
177
195
  }
178
196
  } catch (err) {
179
- console.error(`Error: ${err.message}`);
197
+ console.error(ui.error(err.message));
180
198
  process.exit(1);
181
199
  } finally {
182
200
  if (client) await client.close();
package/src/lib/api.js CHANGED
@@ -5,15 +5,19 @@ const MAX_RETRIES = 3;
5
5
 
6
6
  /**
7
7
  * Get the Voyage API key or exit with a helpful error.
8
+ * Checks: env var → config file.
8
9
  * @returns {string}
9
10
  */
10
11
  function requireApiKey() {
11
- const key = process.env.VOYAGE_API_KEY;
12
+ const { getConfigValue } = require('./config');
13
+ const key = process.env.VOYAGE_API_KEY || getConfigValue('apiKey');
12
14
  if (!key) {
13
- console.error('Error: VOYAGE_API_KEY environment variable is not set.');
15
+ console.error('Error: VOYAGE_API_KEY is not set.');
16
+ console.error('');
17
+ console.error('Option 1: export VOYAGE_API_KEY="your-key-here"');
18
+ console.error('Option 2: vai config set api-key <your-key>');
14
19
  console.error('');
15
20
  console.error('Get one from MongoDB Atlas → AI Models → Create model API key');
16
- console.error('Then: export VOYAGE_API_KEY="your-key-here"');
17
21
  process.exit(1);
18
22
  }
19
23
  return key;
@@ -82,11 +86,11 @@ async function apiRequest(endpoint, body) {
82
86
  * @returns {Promise<object>} API response with embeddings
83
87
  */
84
88
  async function generateEmbeddings(texts, options = {}) {
85
- const { DEFAULT_EMBED_MODEL } = require('./catalog');
89
+ const { getDefaultModel } = require('./catalog');
86
90
 
87
91
  const body = {
88
92
  input: texts,
89
- model: options.model || DEFAULT_EMBED_MODEL,
93
+ model: options.model || getDefaultModel(),
90
94
  };
91
95
 
92
96
  if (options.inputType) {
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const pc = require('picocolors');
4
+
5
+ /**
6
+ * Read the package version from package.json.
7
+ * @returns {string}
8
+ */
9
+ function getVersion() {
10
+ const pkg = require('../../package.json');
11
+ return pkg.version || '0.0.0';
12
+ }
13
+
14
+ /**
15
+ * Display a compact ASCII banner for the CLI.
16
+ */
17
+ function showBanner() {
18
+ const version = getVersion();
19
+ const title = ` 🧭 ${pc.bold(pc.cyan('vai'))} — ${pc.bold('Voyage AI CLI')} ${pc.dim('v' + version)}`;
20
+ const tagline = ` ${pc.dim('Embeddings, reranking & search')}`;
21
+
22
+ // Calculate visible width (strip ANSI codes for alignment)
23
+ const stripAnsi = (str) => str.replace(/\x1b\[[0-9;]*m/g, '');
24
+ const titleLen = stripAnsi(title).length;
25
+ const taglineLen = stripAnsi(tagline).length;
26
+ const innerWidth = Math.max(titleLen, taglineLen) + 2;
27
+
28
+ const top = pc.dim(' ╭' + '─'.repeat(innerWidth) + '╮');
29
+ const bot = pc.dim(' ╰' + '─'.repeat(innerWidth) + '╯');
30
+ const titleLine = pc.dim(' │') + title + ' '.repeat(innerWidth - titleLen) + pc.dim('│');
31
+ const taglineLine = pc.dim(' │') + tagline + ' '.repeat(innerWidth - taglineLen) + pc.dim('│');
32
+
33
+ console.log('');
34
+ console.log(top);
35
+ console.log(titleLine);
36
+ console.log(taglineLine);
37
+ console.log(bot);
38
+ console.log('');
39
+ }
40
+
41
+ /**
42
+ * Display the quick start guide with colored commands.
43
+ */
44
+ function showQuickStart() {
45
+ console.log(` ${pc.bold('Quick start:')}`);
46
+ console.log(` ${pc.cyan('$ vai ping')} Test your connection`);
47
+ console.log(` ${pc.cyan('$ vai embed "hello world"')} Generate embeddings`);
48
+ console.log(` ${pc.cyan('$ vai models')} List available models`);
49
+ console.log(` ${pc.cyan('$ vai demo')} Interactive walkthrough`);
50
+ console.log('');
51
+ console.log(` Run ${pc.cyan('vai <command> --help')} for detailed usage.`);
52
+ console.log('');
53
+ }
54
+
55
+ module.exports = { showBanner, showQuickStart, getVersion };
@@ -1,9 +1,27 @@
1
1
  'use strict';
2
2
 
3
+ const { getConfigValue } = require('./config');
4
+
3
5
  const DEFAULT_EMBED_MODEL = 'voyage-4-large';
4
6
  const DEFAULT_RERANK_MODEL = 'rerank-2.5';
5
7
  const DEFAULT_DIMENSIONS = 1024;
6
8
 
9
+ /**
10
+ * Get the default embedding model (config override or built-in default).
11
+ * @returns {string}
12
+ */
13
+ function getDefaultModel() {
14
+ return getConfigValue('defaultModel') || DEFAULT_EMBED_MODEL;
15
+ }
16
+
17
+ /**
18
+ * Get the default dimensions (config override or built-in default).
19
+ * @returns {number}
20
+ */
21
+ function getDefaultDimensions() {
22
+ return getConfigValue('defaultDimensions') || DEFAULT_DIMENSIONS;
23
+ }
24
+
7
25
  /** @type {Array<{name: string, type: string, context: string, dimensions: string, price: string, bestFor: string}>} */
8
26
  const MODEL_CATALOG = [
9
27
  { name: 'voyage-4-large', type: 'embedding', context: '32K', dimensions: '1024 (default), 256, 512, 2048', price: '$0.12/1M tokens', bestFor: 'Best quality, multilingual' },
@@ -22,5 +40,7 @@ module.exports = {
22
40
  DEFAULT_EMBED_MODEL,
23
41
  DEFAULT_RERANK_MODEL,
24
42
  DEFAULT_DIMENSIONS,
43
+ getDefaultModel,
44
+ getDefaultDimensions,
25
45
  MODEL_CATALOG,
26
46
  };