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.
@@ -0,0 +1,281 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Preflight checks for vai chat.
5
+ *
6
+ * Validates that the full RAG pipeline is ready before
7
+ * starting a chat session. Returns structured results
8
+ * usable by CLI, Playground, and Desktop.
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} PreflightCheck
13
+ * @property {string} id - check identifier
14
+ * @property {string} label - human-readable label
15
+ * @property {boolean} ok - passed?
16
+ * @property {string} [detail] - success detail (e.g. "23,530 documents")
17
+ * @property {string} [error] - failure message
18
+ * @property {string[]} [fix] - commands to fix the issue
19
+ */
20
+
21
+ /**
22
+ * Run all preflight checks for chat.
23
+ *
24
+ * @param {object} params
25
+ * @param {string} params.db - database name
26
+ * @param {string} params.collection - collection name
27
+ * @param {string} params.field - embedding field name (default: 'embedding')
28
+ * @param {object} params.llmConfig - resolved LLM config ({ provider, model, ... })
29
+ * @param {string} [params.textField] - document text field (default: 'text')
30
+ * @returns {Promise<{ checks: PreflightCheck[], ready: boolean }>}
31
+ */
32
+ async function runPreflight({ db, collection, field = 'embedding', llmConfig, textField = 'text' }) {
33
+ const checks = [];
34
+
35
+ // 1. LLM Provider
36
+ checks.push({
37
+ id: 'llm',
38
+ label: 'LLM Provider',
39
+ ok: !!llmConfig?.provider,
40
+ detail: llmConfig?.provider
41
+ ? `${llmConfig.provider} (${llmConfig.model})`
42
+ : undefined,
43
+ error: !llmConfig?.provider ? 'No LLM provider configured' : undefined,
44
+ fix: !llmConfig?.provider ? [
45
+ 'vai config set llm-provider anthropic',
46
+ 'vai config set llm-api-key YOUR_KEY',
47
+ ] : undefined,
48
+ });
49
+
50
+ // 2–4: MongoDB checks (need connection)
51
+ let client;
52
+ try {
53
+ const { getMongoCollection } = require('./mongo');
54
+ const result = await getMongoCollection(db, collection);
55
+ client = result.client;
56
+ const coll = result.collection;
57
+
58
+ // 2. Collection + document count
59
+ const docCount = await coll.estimatedDocumentCount();
60
+ if (docCount === 0) {
61
+ checks.push({
62
+ id: 'collection',
63
+ label: 'Collection',
64
+ ok: false,
65
+ error: `${db}.${collection} is empty (0 documents)`,
66
+ fix: [
67
+ `vai pipeline ./your-docs --db ${db} --collection ${collection}`,
68
+ ],
69
+ });
70
+ } else {
71
+ checks.push({
72
+ id: 'collection',
73
+ label: 'Collection',
74
+ ok: true,
75
+ detail: `${db}.${collection} (${docCount.toLocaleString()} documents)`,
76
+ });
77
+ }
78
+
79
+ // 3. Embeddings — check if documents have the embedding field
80
+ if (docCount > 0) {
81
+ const withEmbedding = await coll.countDocuments(
82
+ { [field]: { $exists: true } },
83
+ { limit: 1 }
84
+ );
85
+ if (withEmbedding === 0) {
86
+ checks.push({
87
+ id: 'embeddings',
88
+ label: 'Embeddings',
89
+ ok: false,
90
+ error: `No '${field}' field found in documents`,
91
+ fix: [
92
+ `vai pipeline ./your-docs --db ${db} --collection ${collection}`,
93
+ '',
94
+ 'Or step by step:',
95
+ ` vai chunk ./docs # Split into chunks`,
96
+ ` vai store --db ${db} --collection ${collection} # Embed and store`,
97
+ ],
98
+ });
99
+ } else {
100
+ // Check what fraction have embeddings
101
+ const embeddedCount = await coll.countDocuments({ [field]: { $exists: true } });
102
+ const pct = Math.round((embeddedCount / docCount) * 100);
103
+ checks.push({
104
+ id: 'embeddings',
105
+ label: 'Embeddings',
106
+ ok: true,
107
+ detail: pct === 100
108
+ ? `All documents have '${field}' field`
109
+ : `${embeddedCount.toLocaleString()}/${docCount.toLocaleString()} documents embedded (${pct}%)`,
110
+ });
111
+ }
112
+ } else {
113
+ checks.push({
114
+ id: 'embeddings',
115
+ label: 'Embeddings',
116
+ ok: false,
117
+ error: 'No documents to check',
118
+ });
119
+ }
120
+
121
+ // 4. Vector search index
122
+ try {
123
+ const indexes = await coll.listSearchIndexes().toArray();
124
+ const vectorIndex = indexes.find(idx => {
125
+ // Check if any index has a vector field mapping
126
+ if (idx.latestDefinition?.fields) {
127
+ return idx.latestDefinition.fields.some(
128
+ f => f.type === 'vector' || f.type === 'knnVector'
129
+ );
130
+ }
131
+ // Atlas Search index with vectorSearch type
132
+ if (idx.type === 'vectorSearch') return true;
133
+ return false;
134
+ });
135
+
136
+ if (vectorIndex) {
137
+ const status = vectorIndex.status || 'READY';
138
+ const building = status !== 'READY' && status !== 'FAILED';
139
+ checks.push({
140
+ id: 'vectorIndex',
141
+ label: 'Vector Search Index',
142
+ ok: status === 'READY',
143
+ building,
144
+ indexName: vectorIndex.name,
145
+ status,
146
+ detail: status === 'READY'
147
+ ? `'${vectorIndex.name}' (${status})`
148
+ : undefined,
149
+ error: status !== 'READY'
150
+ ? `Index '${vectorIndex.name}' status: ${status}`
151
+ : undefined,
152
+ });
153
+ } else {
154
+ checks.push({
155
+ id: 'vectorIndex',
156
+ label: 'Vector Search Index',
157
+ ok: false,
158
+ error: `No vector search index found on ${db}.${collection}`,
159
+ fix: [
160
+ `vai index create --db ${db} --collection ${collection} --field ${field} --dimensions 1024`,
161
+ ],
162
+ });
163
+ }
164
+ } catch (err) {
165
+ // listSearchIndexes may not be available on non-Atlas deployments
166
+ checks.push({
167
+ id: 'vectorIndex',
168
+ label: 'Vector Search Index',
169
+ ok: false,
170
+ error: `Could not check indexes: ${err.message}`,
171
+ fix: [
172
+ `vai index create --db ${db} --collection ${collection} --field ${field} --dimensions 1024`,
173
+ ],
174
+ });
175
+ }
176
+
177
+ } catch (err) {
178
+ // MongoDB connection failed entirely
179
+ checks.push({
180
+ id: 'collection',
181
+ label: 'MongoDB Connection',
182
+ ok: false,
183
+ error: err.message,
184
+ fix: [
185
+ 'vai config set mongodb-uri "mongodb+srv://user:pass@cluster.mongodb.net/"',
186
+ ],
187
+ });
188
+ } finally {
189
+ if (client) {
190
+ try { await client.close(); } catch { /* ignore */ }
191
+ }
192
+ }
193
+
194
+ const ready = checks.every(c => c.ok);
195
+ return { checks, ready };
196
+ }
197
+
198
+ /**
199
+ * Format preflight results for terminal display.
200
+ * @param {PreflightCheck[]} checks
201
+ * @returns {string}
202
+ */
203
+ function formatPreflight(checks) {
204
+ const pc = require('picocolors');
205
+ const lines = [];
206
+
207
+ for (const check of checks) {
208
+ const icon = check.ok ? pc.green('✓') : pc.red('✗');
209
+ const detail = check.ok ? pc.dim(check.detail || '') : pc.red(check.error || 'failed');
210
+ lines.push(` ${icon} ${pc.bold(padRight(check.label, 22))} ${detail}`);
211
+ }
212
+
213
+ // Collect all fix commands from failed checks
214
+ const failedChecks = checks.filter(c => !c.ok && c.fix);
215
+ if (failedChecks.length > 0) {
216
+ lines.push('');
217
+ lines.push(pc.bold(' To fix:'));
218
+ for (const check of failedChecks) {
219
+ for (const cmd of check.fix) {
220
+ if (cmd === '') {
221
+ lines.push('');
222
+ } else if (cmd.startsWith(' ') || cmd.startsWith('Or ')) {
223
+ lines.push(` ${pc.dim(cmd)}`);
224
+ } else {
225
+ lines.push(` ${pc.cyan(cmd)}`);
226
+ }
227
+ }
228
+ }
229
+ lines.push('');
230
+ lines.push(` ${pc.dim('Learn more: vai explain chat')}`);
231
+ }
232
+
233
+ return lines.join('\n');
234
+ }
235
+
236
+ function padRight(str, len) {
237
+ return str + ' '.repeat(Math.max(0, len - str.length));
238
+ }
239
+
240
+ /**
241
+ * Poll a vector search index until it's READY or timeout.
242
+ *
243
+ * @param {object} params
244
+ * @param {string} params.db
245
+ * @param {string} params.collection
246
+ * @param {string} params.indexName
247
+ * @param {number} [params.timeoutMs] - max wait time (default 5 min)
248
+ * @param {number} [params.pollMs] - poll interval (default 5s)
249
+ * @returns {Promise<{ ready: boolean, status: string, elapsed: number }>}
250
+ */
251
+ async function waitForIndex({ db, collection, indexName, timeoutMs = 300000, pollMs = 5000 }) {
252
+ const { getMongoCollection } = require('./mongo');
253
+ let client;
254
+ try {
255
+ const result = await getMongoCollection(db, collection);
256
+ client = result.client;
257
+ const coll = result.collection;
258
+
259
+ const start = Date.now();
260
+ while (Date.now() - start < timeoutMs) {
261
+ const indexes = await coll.listSearchIndexes().toArray();
262
+ const idx = indexes.find(i => i.name === indexName);
263
+ if (!idx) return { ready: false, status: 'NOT_FOUND', elapsed: Date.now() - start };
264
+ if (idx.status === 'READY') return { ready: true, status: 'READY', elapsed: Date.now() - start };
265
+ if (idx.status === 'FAILED') return { ready: false, status: 'FAILED', elapsed: Date.now() - start };
266
+
267
+ await new Promise(r => setTimeout(r, pollMs));
268
+ }
269
+ return { ready: false, status: 'TIMEOUT', elapsed: Date.now() - start };
270
+ } finally {
271
+ if (client) {
272
+ try { await client.close(); } catch { /* ignore */ }
273
+ }
274
+ }
275
+ }
276
+
277
+ module.exports = {
278
+ runPreflight,
279
+ formatPreflight,
280
+ waitForIndex,
281
+ };
@@ -0,0 +1,111 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Prompt Builder
5
+ *
6
+ * Constructs the message array sent to the LLM from
7
+ * retrieved documents, conversation history, and user query.
8
+ */
9
+
10
+ const DEFAULT_SYSTEM_PROMPT = `You are an assistant powered by a retrieval-augmented generation (RAG) pipeline built with Voyage AI embeddings and MongoDB Atlas Vector Search. Your answers are grounded in documents retrieved from the user's knowledge base.
11
+
12
+ ## How to use the retrieved context
13
+
14
+ - Each context document includes a source label and a relevance score (0 to 1). Higher scores indicate stronger semantic matches to the user's query.
15
+ - Treat documents with scores below 0.3 as weak matches. If only weak matches were retrieved, say so rather than forcing an answer from them.
16
+ - When documents conflict, surface the discrepancy and let the user decide which to trust.
17
+
18
+ ## Answering rules
19
+
20
+ 1. Ground every claim in the provided context. Do not supplement with outside knowledge unless you explicitly flag it as such (e.g. "Outside the retrieved documents, ...").
21
+ 2. Cite sources inline using the format [Source: <label>]. Use the source labels from the context block.
22
+ 3. If the context is insufficient, say so directly. Suggest how the user might refine their query or expand their knowledge base.
23
+ 4. Be concise. Prefer short, direct answers. Use lists or structure when it aids clarity.
24
+ 5. For follow-up questions, rely on the newly retrieved context for that turn. Prior context may be stale.`;
25
+
26
+ /**
27
+ * Format retrieved documents into a context block.
28
+ * @param {Array<{source: string, text: string, score: number}>} docs
29
+ * @returns {string}
30
+ */
31
+ function formatContextBlock(docs) {
32
+ if (!docs || docs.length === 0) return '';
33
+
34
+ const lines = ['--- Context Documents ---', ''];
35
+
36
+ for (const doc of docs) {
37
+ const source = doc.source || doc.metadata?.source || 'unknown';
38
+ const score = doc.score != null ? doc.score.toFixed(2) : 'N/A';
39
+ lines.push(`[Source: ${source} | Relevance: ${score}]`);
40
+ lines.push(doc.text || doc.chunk || '');
41
+ lines.push('');
42
+ }
43
+
44
+ lines.push('--- End Context ---');
45
+ return lines.join('\n');
46
+ }
47
+
48
+ /**
49
+ * Build the full system prompt.
50
+ *
51
+ * The base prompt (grounding rules, citation format, safety guardrails)
52
+ * is always included. Users can append custom instructions via
53
+ * `systemPrompt` — these are added after the base, not replacing it.
54
+ *
55
+ * @param {string} [customPrompt] - User's custom instructions (appended, not replacing)
56
+ * @returns {string}
57
+ */
58
+ function buildSystemPrompt(customPrompt) {
59
+ if (!customPrompt) return DEFAULT_SYSTEM_PROMPT;
60
+
61
+ return `${DEFAULT_SYSTEM_PROMPT}
62
+
63
+ ## Additional Instructions
64
+
65
+ ${customPrompt}`;
66
+ }
67
+
68
+ /**
69
+ * Build the message array for the LLM.
70
+ *
71
+ * @param {object} params
72
+ * @param {string} params.query - Current user question
73
+ * @param {Array} params.contextDocs - Retrieved + reranked documents
74
+ * @param {Array} [params.history] - Previous conversation turns [{role, content}]
75
+ * @param {string} [params.systemPrompt] - Custom instructions (appended to base prompt)
76
+ * @returns {Array<{role: string, content: string}>}
77
+ */
78
+ function buildMessages({ query, contextDocs = [], history = [], systemPrompt }) {
79
+ const messages = [];
80
+
81
+ // 1. System prompt (base + custom instructions)
82
+ messages.push({
83
+ role: 'system',
84
+ content: buildSystemPrompt(systemPrompt),
85
+ });
86
+
87
+ // 2. Conversation history (previous turns)
88
+ for (const turn of history) {
89
+ messages.push({ role: turn.role, content: turn.content });
90
+ }
91
+
92
+ // 3. Current user message with injected context
93
+ const contextBlock = formatContextBlock(contextDocs);
94
+ let userContent = '';
95
+ if (contextBlock) {
96
+ userContent = `${contextBlock}\n\nUser question: ${query}`;
97
+ } else {
98
+ userContent = query;
99
+ }
100
+
101
+ messages.push({ role: 'user', content: userContent });
102
+
103
+ return messages;
104
+ }
105
+
106
+ module.exports = {
107
+ DEFAULT_SYSTEM_PROMPT,
108
+ buildSystemPrompt,
109
+ formatContextBlock,
110
+ buildMessages,
111
+ };
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * CLI renderer for the wizard engine.
5
+ *
6
+ * Uses @clack/prompts for beautiful terminal UI with
7
+ * back-navigation support (type "back" or "<" at any prompt).
8
+ */
9
+
10
+ // Lazy-loaded — @clack/prompts is ESM-only in some versions,
11
+ // and eager require crashes on Node <22. Only loaded when actually used.
12
+ let p;
13
+ let pc;
14
+ function ensureDeps() {
15
+ if (!p) p = require('@clack/prompts');
16
+ if (!pc) pc = require('picocolors');
17
+ }
18
+
19
+ const BACK = Symbol.for('back');
20
+ const CANCEL = Symbol.for('cancel');
21
+
22
+ /**
23
+ * Create a CLI renderer for runWizard().
24
+ *
25
+ * @param {object} [opts]
26
+ * @param {string} [opts.title] - intro title
27
+ * @param {string} [opts.doneMessage] - outro message
28
+ * @param {boolean} [opts.showBackHint] - show "type < to go back" hint (default true)
29
+ * @returns {object} renderer compatible with runWizard
30
+ */
31
+ function createCLIRenderer(opts = {}) {
32
+ ensureDeps();
33
+ const showBackHint = opts.showBackHint !== false;
34
+
35
+ return {
36
+ async intro(steps, config) {
37
+ if (opts.title) {
38
+ p.intro(pc.bold(opts.title));
39
+ }
40
+ },
41
+
42
+ async prompt(step, ctx) {
43
+ const { options, defaultValue, stepNumber, totalSteps, isFirst } = ctx;
44
+ const backHint = (!isFirst && showBackHint)
45
+ ? pc.dim(' (< to go back)')
46
+ : '';
47
+ const stepLabel = pc.dim(`[${stepNumber}/${totalSteps}]`);
48
+ const label = `${stepLabel} ${step.label}${backHint}`;
49
+
50
+ let result;
51
+
52
+ switch (step.type) {
53
+ case 'select': {
54
+ // Map options to clack format
55
+ const clackOptions = options.map(o => ({
56
+ value: o.value,
57
+ label: o.label,
58
+ hint: o.hint || undefined,
59
+ }));
60
+
61
+ // Add "Go back" option if not first step
62
+ if (!isFirst) {
63
+ clackOptions.push({
64
+ value: '__back__',
65
+ label: pc.dim('← Go back'),
66
+ });
67
+ }
68
+
69
+ result = await p.select({
70
+ message: label,
71
+ options: clackOptions,
72
+ initialValue: defaultValue || undefined,
73
+ });
74
+
75
+ if (p.isCancel(result)) return CANCEL;
76
+ if (result === '__back__') return BACK;
77
+ return result;
78
+ }
79
+
80
+ case 'text':
81
+ case 'password': {
82
+ result = await p.text({
83
+ message: label,
84
+ placeholder: step.placeholder || '',
85
+ defaultValue: defaultValue != null ? String(defaultValue) : undefined,
86
+ validate: (val) => {
87
+ if (val == null) return undefined;
88
+ // Handle back navigation
89
+ if (val === '<' || (typeof val === 'string' && val.toLowerCase() === 'back')) return undefined;
90
+ if (step.required && !val) return `${step.label} is required`;
91
+ if (step.validate) {
92
+ const v = step.validate(val, ctx.answers);
93
+ if (v !== true) return v;
94
+ }
95
+ return undefined;
96
+ },
97
+ });
98
+
99
+ if (p.isCancel(result)) return CANCEL;
100
+ if (result === '<' || (typeof result === 'string' && result.toLowerCase() === 'back')) return BACK;
101
+ return result;
102
+ }
103
+
104
+ case 'confirm': {
105
+ result = await p.confirm({
106
+ message: label,
107
+ initialValue: defaultValue != null ? defaultValue : true,
108
+ });
109
+
110
+ if (p.isCancel(result)) return CANCEL;
111
+ return result;
112
+ }
113
+
114
+ default:
115
+ throw new Error(`Unknown step type: ${step.type}`);
116
+ }
117
+ },
118
+
119
+ async error(message) {
120
+ p.log.error(message);
121
+ },
122
+
123
+ async cancel() {
124
+ p.cancel('Setup cancelled.');
125
+ },
126
+
127
+ async outro(answers) {
128
+ if (opts.doneMessage) {
129
+ p.outro(opts.doneMessage);
130
+ }
131
+ },
132
+ };
133
+ }
134
+
135
+ module.exports = { createCLIRenderer };
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Chat setup wizard step definitions.
5
+ *
6
+ * Surface-agnostic — consumed by CLI (@clack/prompts),
7
+ * Playground (React), and Desktop (Electron/LeafyGreen).
8
+ *
9
+ * Each step is a plain declarative object. Dynamic values
10
+ * (options, defaults, skip predicates) are functions of
11
+ * (answers, config) so renderers can evaluate them.
12
+ */
13
+
14
+ const { PROVIDER_DEFAULTS, PROVIDER_MODELS, listOllamaModels } = require('./llm');
15
+
16
+ // Cache Ollama detection across steps
17
+ let _ollamaModels = null;
18
+ let _ollamaChecked = false;
19
+
20
+ async function detectOllama() {
21
+ if (_ollamaChecked) return _ollamaModels;
22
+ _ollamaChecked = true;
23
+ try {
24
+ _ollamaModels = await listOllamaModels({ timeoutMs: 2000 });
25
+ } catch {
26
+ _ollamaModels = [];
27
+ }
28
+ return _ollamaModels;
29
+ }
30
+
31
+ /**
32
+ * Reset Ollama cache (for testing).
33
+ */
34
+ function resetOllamaCache() {
35
+ _ollamaModels = null;
36
+ _ollamaChecked = false;
37
+ }
38
+
39
+ /**
40
+ * Build provider options dynamically — detects Ollama availability.
41
+ * @returns {Promise<Array<{value: string, label: string, hint: string}>>}
42
+ */
43
+ async function getProviderOptions() {
44
+ const ollamaModels = await detectOllama();
45
+ const ollamaAvailable = ollamaModels && ollamaModels.length > 0;
46
+
47
+ const options = [
48
+ {
49
+ value: 'anthropic',
50
+ label: 'Anthropic (Claude)',
51
+ hint: 'Best instruction following',
52
+ },
53
+ {
54
+ value: 'openai',
55
+ label: 'OpenAI (GPT-4o)',
56
+ hint: 'Broad model selection',
57
+ },
58
+ {
59
+ value: 'ollama',
60
+ label: ollamaAvailable
61
+ ? `Ollama (${ollamaModels.length} model${ollamaModels.length === 1 ? '' : 's'} installed)`
62
+ : 'Ollama (local — not detected)',
63
+ hint: ollamaAvailable ? 'Free, fully private' : 'Requires ollama running locally',
64
+ },
65
+ ];
66
+
67
+ return options;
68
+ }
69
+
70
+ /**
71
+ * Build model options for the selected provider.
72
+ */
73
+ async function getModelOptions(answers) {
74
+ const provider = answers.provider;
75
+ if (!provider) return [];
76
+
77
+ if (provider === 'ollama') {
78
+ const models = await detectOllama();
79
+ if (models.length === 0) {
80
+ return [{ value: 'llama3.1', label: 'llama3.1', hint: 'default (pull with: ollama pull llama3.1)' }];
81
+ }
82
+ return models.map(m => ({
83
+ value: m.id,
84
+ label: m.name,
85
+ hint: [m.parameterSize, m.size].filter(Boolean).join(' — ') || undefined,
86
+ }));
87
+ }
88
+
89
+ const cloudModels = PROVIDER_MODELS[provider] || [];
90
+ return cloudModels.map(m => ({
91
+ value: m.id,
92
+ label: m.name,
93
+ hint: m.context ? `context: ${m.context}` : undefined,
94
+ }));
95
+ }
96
+
97
+ /**
98
+ * The chat setup wizard steps.
99
+ *
100
+ * These are consumed by:
101
+ * - CLI: wizard-cli.js (via runWizard)
102
+ * - Playground: Settings panel in Chat tab
103
+ * - Desktop: Chat settings in Electron app
104
+ */
105
+ const chatSetupSteps = [
106
+ {
107
+ id: 'provider',
108
+ label: 'LLM Provider',
109
+ type: 'select',
110
+ options: () => getProviderOptions(),
111
+ required: true,
112
+ skip: (_answers, config) => !!config.llmProvider,
113
+ group: 'LLM Configuration',
114
+ },
115
+
116
+ {
117
+ id: 'apiKey',
118
+ label: 'API Key',
119
+ type: 'password',
120
+ required: true,
121
+ placeholder: 'sk-...',
122
+ skip: (answers, config) => {
123
+ // Skip for Ollama (no key needed)
124
+ const provider = answers.provider || config.llmProvider;
125
+ if (provider === 'ollama') return true;
126
+ // Skip if already configured
127
+ if (config.llmApiKey) return true;
128
+ return false;
129
+ },
130
+ validate: (value) => {
131
+ if (!value || value.length < 8) return 'API key looks too short';
132
+ return true;
133
+ },
134
+ group: 'LLM Configuration',
135
+ },
136
+
137
+ {
138
+ id: 'model',
139
+ label: 'Model',
140
+ type: 'select',
141
+ options: (answers) => getModelOptions(answers),
142
+ getDefault: (answers, config) => {
143
+ const provider = answers.provider || config.llmProvider;
144
+ return config.llmModel || PROVIDER_DEFAULTS[provider] || null;
145
+ },
146
+ required: false, // uses provider default if skipped
147
+ group: 'LLM Configuration',
148
+ },
149
+
150
+ {
151
+ id: 'ollamaBaseUrl',
152
+ label: 'Ollama URL',
153
+ type: 'text',
154
+ defaultValue: 'http://localhost:11434',
155
+ placeholder: 'http://localhost:11434',
156
+ required: false,
157
+ skip: (answers, config) => {
158
+ const provider = answers.provider || config.llmProvider;
159
+ return provider !== 'ollama';
160
+ },
161
+ group: 'LLM Configuration',
162
+ },
163
+ ];
164
+
165
+ module.exports = {
166
+ chatSetupSteps,
167
+ getProviderOptions,
168
+ getModelOptions,
169
+ detectOllama,
170
+ resetOllamaCache,
171
+ };