@gmickel/gno 0.6.0 → 0.7.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/README.md CHANGED
@@ -36,6 +36,8 @@ gno query "auth best practices" # Hybrid search
36
36
  gno ask "summarize the API" --answer # AI answer with citations
37
37
  ```
38
38
 
39
+ ![GNO CLI](./assets/screenshots/cli.jpg)
40
+
39
41
  ---
40
42
 
41
43
  ## Installation
@@ -127,6 +129,8 @@ gno serve # Start on port 3000
127
129
  gno serve --port 8080 # Custom port
128
130
  ```
129
131
 
132
+ ![GNO Web UI](./assets/screenshots/webui-home.jpg)
133
+
130
134
  Open `http://localhost:3000` to:
131
135
 
132
136
  - **Search** — BM25, vector, or hybrid modes with visual results
@@ -181,6 +185,8 @@ No authentication. No rate limits. Build custom tools, automate workflows, integ
181
185
 
182
186
  ### MCP Server
183
187
 
188
+ ![GNO MCP](./assets/screenshots/mcp.jpg)
189
+
184
190
  GNO exposes 6 tools via [Model Context Protocol](https://modelcontextprotocol.io):
185
191
 
186
192
  | Tool | Description |
@@ -202,6 +208,8 @@ Skills add GNO search to Claude Code/Codex without MCP protocol overhead:
202
208
  gno skill install --scope user
203
209
  ```
204
210
 
211
+ ![GNO Skill in Claude Code](./assets/screenshots/claudecodeskill.jpg)
212
+
205
213
  Then ask your agent: *"Search my notes for the auth discussion"*
206
214
 
207
215
  > **Detailed docs**: [MCP Integration](https://gno.sh/docs/MCP/) · [Use Cases](https://gno.sh/docs/USE-CASES/)
@@ -280,7 +288,7 @@ Models auto-download on first use to `~/.cache/gno/models/`.
280
288
 
281
289
  ```bash
282
290
  gno models use balanced
283
- gno models pull --all
291
+ gno models pull --all # Optional: pre-download models (auto-downloads on first use)
284
292
  ```
285
293
 
286
294
  > **Configuration**: [Model Setup](https://gno.sh/docs/CONFIGURATION/)
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "search",
@@ -33,7 +33,7 @@
33
33
  "vendor"
34
34
  ],
35
35
  "engines": {
36
- "bun": ">=1.0.0"
36
+ "bun": ">=1.3.0"
37
37
  },
38
38
  "publishConfig": {
39
39
  "access": "public"
@@ -113,11 +113,11 @@
113
113
  "evalite": "^1.0.0-beta.15",
114
114
  "exceljs": "^4.4.0",
115
115
  "lefthook": "^2.0.13",
116
- "oxlint-tsgolint": "^0.10.0",
116
+ "oxlint-tsgolint": "^0.10.1",
117
117
  "pdf-lib": "^1.17.1",
118
118
  "pptxgenjs": "^4.0.1",
119
119
  "tailwindcss": "^4.1.18",
120
- "ultracite": "6.5.0"
120
+ "ultracite": "7.0.4"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "typescript": "^5"
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
9
+ import { resolveDownloadPolicy } from '../../llm/policy';
9
10
  import { getActivePreset } from '../../llm/registry';
10
11
  import type {
11
12
  EmbeddingPort,
@@ -22,6 +23,11 @@ import {
22
23
  createVectorIndexPort,
23
24
  type VectorIndexPort,
24
25
  } from '../../store/vector';
26
+ import { getGlobals } from '../program';
27
+ import {
28
+ createProgressRenderer,
29
+ createThrottledProgressRenderer,
30
+ } from '../progress';
25
31
  import { initStore } from './shared';
26
32
 
27
33
  // ─────────────────────────────────────────────────────────────────────────────
@@ -82,9 +88,26 @@ export async function ask(
82
88
  const preset = getActivePreset(config);
83
89
  const llm = new LlmAdapter(config);
84
90
 
91
+ // Resolve download policy from env/flags
92
+ const globals = getGlobals();
93
+ const policy = resolveDownloadPolicy(process.env, {
94
+ offline: globals.offline,
95
+ });
96
+
97
+ // Create progress renderer for model downloads (throttled)
98
+ const showProgress = !options.json && process.stderr.isTTY;
99
+ const downloadProgress = showProgress
100
+ ? createThrottledProgressRenderer(createProgressRenderer())
101
+ : undefined;
102
+
85
103
  // Create embedding port
86
104
  const embedUri = options.embedModel ?? preset.embed;
87
- const embedResult = await llm.createEmbeddingPort(embedUri);
105
+ const embedResult = await llm.createEmbeddingPort(embedUri, {
106
+ policy,
107
+ onProgress: downloadProgress
108
+ ? (progress) => downloadProgress('embed', progress)
109
+ : undefined,
110
+ });
88
111
  if (embedResult.ok) {
89
112
  embedPort = embedResult.value;
90
113
  }
@@ -94,7 +117,12 @@ export async function ask(
94
117
  const needsGen = !options.noExpand || options.answer;
95
118
  if (needsGen) {
96
119
  const genUri = options.genModel ?? preset.gen;
97
- const genResult = await llm.createGenerationPort(genUri);
120
+ const genResult = await llm.createGenerationPort(genUri, {
121
+ policy,
122
+ onProgress: downloadProgress
123
+ ? (progress) => downloadProgress('gen', progress)
124
+ : undefined,
125
+ });
98
126
  if (genResult.ok) {
99
127
  genPort = genResult.value;
100
128
  }
@@ -103,12 +131,22 @@ export async function ask(
103
131
  // Create rerank port (unless --fast or --no-rerank)
104
132
  if (!options.noRerank) {
105
133
  const rerankUri = options.rerankModel ?? preset.rerank;
106
- const rerankResult = await llm.createRerankPort(rerankUri);
134
+ const rerankResult = await llm.createRerankPort(rerankUri, {
135
+ policy,
136
+ onProgress: downloadProgress
137
+ ? (progress) => downloadProgress('rerank', progress)
138
+ : undefined,
139
+ });
107
140
  if (rerankResult.ok) {
108
141
  rerankPort = rerankResult.value;
109
142
  }
110
143
  }
111
144
 
145
+ // Clear progress line if shown
146
+ if (showProgress && downloadProgress) {
147
+ process.stderr.write('\n');
148
+ }
149
+
112
150
  // Create vector index
113
151
  let vectorIndex: VectorIndexPort | null = null;
114
152
  if (embedPort) {
@@ -2,11 +2,8 @@
2
2
  * gno collection add - Add a new collection
3
3
  */
4
4
 
5
+ import { addCollection } from '../../../collection';
5
6
  import {
6
- type Collection,
7
- CollectionSchema,
8
- DEFAULT_EXCLUDES,
9
- DEFAULT_PATTERN,
10
7
  loadConfig,
11
8
  pathExists,
12
9
  saveConfig,
@@ -31,83 +28,45 @@ export async function collectionAdd(
31
28
  throw new CliError('VALIDATION', '--name is required');
32
29
  }
33
30
 
34
- const collectionName = options.name.toLowerCase();
35
-
36
- // Expand and validate path
31
+ // Validate path exists BEFORE loading config (user-friendly error ordering)
37
32
  const absolutePath = toAbsolutePath(path);
38
-
39
- // Check if path exists
40
33
  const exists = await pathExists(absolutePath);
41
34
  if (!exists) {
42
35
  throw new CliError('VALIDATION', `Path does not exist: ${absolutePath}`);
43
36
  }
44
37
 
45
38
  // Load config
46
- const result = await loadConfig();
47
- if (!result.ok) {
39
+ const configResult = await loadConfig();
40
+ if (!configResult.ok) {
48
41
  throw new CliError(
49
42
  'RUNTIME',
50
- `Failed to load config: ${result.error.message}`
43
+ `Failed to load config: ${configResult.error.message}`
51
44
  );
52
45
  }
53
46
 
54
- const config = result.value;
55
-
56
- // Check for duplicate name
57
- const existing = config.collections.find((c) => c.name === collectionName);
58
- if (existing) {
59
- throw new CliError(
60
- 'VALIDATION',
61
- `Collection "${collectionName}" already exists`
62
- );
63
- }
64
-
65
- // Parse options - filter empty, dedupe
66
- const includeList = options.include
67
- ? [
68
- ...new Set(
69
- options.include
70
- .split(',')
71
- .map((s) => s.trim())
72
- .filter(Boolean)
73
- ),
74
- ]
75
- : [];
76
- const excludeList = options.exclude
77
- ? [
78
- ...new Set(
79
- options.exclude
80
- .split(',')
81
- .map((s) => s.trim())
82
- .filter(Boolean)
83
- ),
84
- ]
85
- : [...DEFAULT_EXCLUDES];
86
-
87
- // Build collection
88
- const collection: Collection = {
89
- name: collectionName,
90
- path: absolutePath,
91
- pattern: options.pattern ?? DEFAULT_PATTERN,
92
- include: includeList,
93
- exclude: excludeList,
47
+ // Add collection using shared module
48
+ const result = await addCollection(configResult.value, {
49
+ path,
50
+ name: options.name,
51
+ pattern: options.pattern,
52
+ include: options.include,
53
+ exclude: options.exclude,
94
54
  updateCmd: options.update,
95
- };
55
+ });
96
56
 
97
- // Validate collection
98
- const validation = CollectionSchema.safeParse(collection);
99
- if (!validation.success) {
100
- throw new CliError(
101
- 'VALIDATION',
102
- `Invalid collection: ${validation.error.issues[0]?.message ?? 'unknown error'}`
103
- );
57
+ if (!result.ok) {
58
+ // Map collection error codes to CLI error codes
59
+ const cliCode =
60
+ result.code === 'VALIDATION' ||
61
+ result.code === 'PATH_NOT_FOUND' ||
62
+ result.code === 'DUPLICATE'
63
+ ? 'VALIDATION'
64
+ : 'RUNTIME';
65
+ throw new CliError(cliCode, result.message);
104
66
  }
105
67
 
106
- // Add to config
107
- config.collections.push(validation.data);
108
-
109
68
  // Save config
110
- const saveResult = await saveConfig(config);
69
+ const saveResult = await saveConfig(result.config);
111
70
  if (!saveResult.ok) {
112
71
  throw new CliError(
113
72
  'RUNTIME',
@@ -115,6 +74,8 @@ export async function collectionAdd(
115
74
  );
116
75
  }
117
76
 
118
- process.stdout.write(`Collection "${collectionName}" added successfully\n`);
119
- process.stdout.write(`Path: ${absolutePath}\n`);
77
+ process.stdout.write(
78
+ `Collection "${result.collection.name}" added successfully\n`
79
+ );
80
+ process.stdout.write(`Path: ${result.collection.path}\n`);
120
81
  }
@@ -2,57 +2,36 @@
2
2
  * gno collection remove - Remove a collection
3
3
  */
4
4
 
5
- import {
6
- getCollectionFromScope,
7
- loadConfig,
8
- saveConfig,
9
- } from '../../../config';
5
+ import { removeCollection } from '../../../collection';
6
+ import { loadConfig, saveConfig } from '../../../config';
10
7
  import { CliError } from '../../errors';
11
8
 
12
9
  export async function collectionRemove(name: string): Promise<void> {
13
- const collectionName = name.toLowerCase();
14
-
15
10
  // Load config
16
- const result = await loadConfig();
17
- if (!result.ok) {
11
+ const configResult = await loadConfig();
12
+ if (!configResult.ok) {
18
13
  throw new CliError(
19
14
  'RUNTIME',
20
- `Failed to load config: ${result.error.message}`
15
+ `Failed to load config: ${configResult.error.message}`
21
16
  );
22
17
  }
23
18
 
24
- const config = result.value;
25
-
26
- // Find collection
27
- const collectionIndex = config.collections.findIndex(
28
- (c) => c.name === collectionName
29
- );
30
- if (collectionIndex === -1) {
31
- throw new CliError(
32
- 'VALIDATION',
33
- `Collection "${collectionName}" not found`
34
- );
35
- }
19
+ // Remove collection using shared module
20
+ const result = removeCollection(configResult.value, { name });
36
21
 
37
- // Check if any contexts reference this collection
38
- const referencingContexts = config.contexts.filter((ctx) => {
39
- const collFromScope = getCollectionFromScope(ctx.scopeKey);
40
- return collFromScope === collectionName;
41
- });
42
-
43
- if (referencingContexts.length > 0) {
44
- const scopes = referencingContexts.map((ctx) => ctx.scopeKey).join(', ');
45
- throw new CliError(
46
- 'VALIDATION',
47
- `Collection "${collectionName}" is referenced by contexts: ${scopes}. Remove the contexts first or rename the collection.`
48
- );
22
+ if (!result.ok) {
23
+ // Map collection error codes to CLI error codes
24
+ const cliCode =
25
+ result.code === 'VALIDATION' ||
26
+ result.code === 'NOT_FOUND' ||
27
+ result.code === 'HAS_REFERENCES'
28
+ ? 'VALIDATION'
29
+ : 'RUNTIME';
30
+ throw new CliError(cliCode, result.message);
49
31
  }
50
32
 
51
- // Remove collection
52
- config.collections.splice(collectionIndex, 1);
53
-
54
33
  // Save config
55
- const saveResult = await saveConfig(config);
34
+ const saveResult = await saveConfig(result.config);
56
35
  if (!saveResult.ok) {
57
36
  throw new CliError(
58
37
  'RUNTIME',
@@ -60,5 +39,7 @@ export async function collectionRemove(name: string): Promise<void> {
60
39
  );
61
40
  }
62
41
 
63
- process.stdout.write(`Collection "${collectionName}" removed successfully\n`);
42
+ process.stdout.write(
43
+ `Collection "${result.collection.name}" removed successfully\n`
44
+ );
64
45
  }
@@ -7,8 +7,14 @@
7
7
 
8
8
  import type { Database } from 'bun:sqlite';
9
9
  import { getIndexDbPath } from '../../app/constants';
10
- import { getConfigPaths, isInitialized, loadConfig } from '../../config';
10
+ import {
11
+ type Config,
12
+ getConfigPaths,
13
+ isInitialized,
14
+ loadConfig,
15
+ } from '../../config';
11
16
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
17
+ import { resolveDownloadPolicy } from '../../llm/policy';
12
18
  import { getActivePreset } from '../../llm/registry';
13
19
  import type { EmbeddingPort } from '../../llm/types';
14
20
  import { formatDocForEmbedding } from '../../pipeline/contextual';
@@ -23,6 +29,11 @@ import {
23
29
  type VectorRow,
24
30
  type VectorStatsPort,
25
31
  } from '../../store/vector';
32
+ import { getGlobals } from '../program';
33
+ import {
34
+ createProgressRenderer,
35
+ createThrottledProgressRenderer,
36
+ } from '../progress';
26
37
 
27
38
  // ─────────────────────────────────────────────────────────────────────────────
28
39
  // Types
@@ -191,35 +202,36 @@ async function processBatches(ctx: BatchContext): Promise<BatchResult> {
191
202
  }
192
203
 
193
204
  // ─────────────────────────────────────────────────────────────────────────────
194
- // Main Command
205
+ // Helpers
195
206
  // ─────────────────────────────────────────────────────────────────────────────
196
207
 
208
+ interface EmbedContext {
209
+ config: Config;
210
+ modelUri: string;
211
+ store: SqliteAdapter;
212
+ }
213
+
197
214
  /**
198
- * Execute gno embed command.
215
+ * Initialize embed context: check init, load config, open store.
199
216
  */
200
- export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
201
- const batchSize = options.batchSize ?? 32;
202
- const force = options.force ?? false;
203
- const dryRun = options.dryRun ?? false;
204
-
205
- // Check initialization
206
- const initialized = await isInitialized(options.configPath);
217
+ async function initEmbedContext(
218
+ configPath?: string,
219
+ model?: string
220
+ ): Promise<({ ok: true } & EmbedContext) | { ok: false; error: string }> {
221
+ const initialized = await isInitialized(configPath);
207
222
  if (!initialized) {
208
- return { success: false, error: 'GNO not initialized. Run: gno init' };
223
+ return { ok: false, error: 'GNO not initialized. Run: gno init' };
209
224
  }
210
225
 
211
- // Load config
212
- const configResult = await loadConfig(options.configPath);
226
+ const configResult = await loadConfig(configPath);
213
227
  if (!configResult.ok) {
214
- return { success: false, error: configResult.error.message };
228
+ return { ok: false, error: configResult.error.message };
215
229
  }
216
230
  const config = configResult.value;
217
231
 
218
- // Get model URI
219
232
  const preset = getActivePreset(config);
220
- const modelUri = options.model ?? preset.embed;
233
+ const modelUri = model ?? preset.embed;
221
234
 
222
- // Open store
223
235
  const store = new SqliteAdapter();
224
236
  const dbPath = getIndexDbPath();
225
237
  const paths = getConfigPaths();
@@ -227,9 +239,31 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
227
239
 
228
240
  const openResult = await store.open(dbPath, config.ftsTokenizer);
229
241
  if (!openResult.ok) {
230
- return { success: false, error: openResult.error.message };
242
+ return { ok: false, error: openResult.error.message };
231
243
  }
232
244
 
245
+ return { ok: true, config, modelUri, store };
246
+ }
247
+
248
+ // ─────────────────────────────────────────────────────────────────────────────
249
+ // Main Command
250
+ // ─────────────────────────────────────────────────────────────────────────────
251
+
252
+ /**
253
+ * Execute gno embed command.
254
+ */
255
+ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
256
+ const batchSize = options.batchSize ?? 32;
257
+ const force = options.force ?? false;
258
+ const dryRun = options.dryRun ?? false;
259
+
260
+ // Initialize config and store
261
+ const initResult = await initEmbedContext(options.configPath, options.model);
262
+ if (!initResult.ok) {
263
+ return { success: false, error: initResult.error };
264
+ }
265
+ const { config, modelUri, store } = initResult;
266
+
233
267
  // Get raw DB for vector ops (SqliteAdapter always implements SqliteDbProvider)
234
268
  const db = store.getRawDb();
235
269
  let embedPort: EmbeddingPort | null = null;
@@ -274,14 +308,35 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
274
308
  };
275
309
  }
276
310
 
277
- // Create LLM adapter and embedding port
311
+ // Create LLM adapter and embedding port with auto-download
312
+ const globals = getGlobals();
313
+ const policy = resolveDownloadPolicy(process.env, {
314
+ offline: globals.offline,
315
+ });
316
+
317
+ // Create progress renderer for model download (throttled to avoid spam)
318
+ const showDownloadProgress = !options.json && process.stderr.isTTY;
319
+ const downloadProgress = showDownloadProgress
320
+ ? createThrottledProgressRenderer(createProgressRenderer())
321
+ : undefined;
322
+
278
323
  const llm = new LlmAdapter(config);
279
- const embedResult = await llm.createEmbeddingPort(modelUri);
324
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
325
+ policy,
326
+ onProgress: downloadProgress
327
+ ? (progress) => downloadProgress('embed', progress)
328
+ : undefined,
329
+ });
280
330
  if (!embedResult.ok) {
281
331
  return { success: false, error: embedResult.error.message };
282
332
  }
283
333
  embedPort = embedResult.value;
284
334
 
335
+ // Clear download progress line if shown
336
+ if (showDownloadProgress) {
337
+ process.stderr.write('\n');
338
+ }
339
+
285
340
  // Discover dimensions via probe embedding
286
341
  const probeResult = await embedPort.embed('dimension probe');
287
342
  if (!probeResult.ok) {
@@ -4,6 +4,7 @@
4
4
  * @module src/cli/commands/models
5
5
  */
6
6
 
7
+ export { createProgressRenderer } from '../../progress';
7
8
  export {
8
9
  formatModelsClear,
9
10
  type ModelsClearOptions,
@@ -23,7 +24,6 @@ export {
23
24
  modelsPath,
24
25
  } from './path';
25
26
  export {
26
- createProgressRenderer,
27
27
  formatModelsPull,
28
28
  type ModelPullResult,
29
29
  type ModelsPullOptions,
@@ -185,20 +185,3 @@ export function formatModelsPull(result: ModelsPullResult): string {
185
185
 
186
186
  return lines.join('\n');
187
187
  }
188
-
189
- /**
190
- * Create a terminal progress renderer.
191
- */
192
- export function createProgressRenderer(): (
193
- type: ModelType,
194
- progress: DownloadProgress
195
- ) => void {
196
- return (type, progress) => {
197
- const percent = progress.percent.toFixed(1);
198
- const downloaded = (progress.downloadedBytes / 1024 / 1024).toFixed(1);
199
- const total = (progress.totalBytes / 1024 / 1024).toFixed(1);
200
- process.stderr.write(
201
- `\r${type}: ${percent}% (${downloaded}/${total} MB) `
202
- );
203
- };
204
- }
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
9
+ import { resolveDownloadPolicy } from '../../llm/policy';
9
10
  import { getActivePreset } from '../../llm/registry';
10
11
  import type {
11
12
  EmbeddingPort,
@@ -18,6 +19,11 @@ import {
18
19
  createVectorIndexPort,
19
20
  type VectorIndexPort,
20
21
  } from '../../store/vector';
22
+ import { getGlobals } from '../program';
23
+ import {
24
+ createProgressRenderer,
25
+ createThrottledProgressRenderer,
26
+ } from '../progress';
21
27
  import { initStore } from './shared';
22
28
 
23
29
  // ─────────────────────────────────────────────────────────────────────────────
@@ -90,9 +96,26 @@ export async function query(
90
96
  const preset = getActivePreset(config);
91
97
  const llm = new LlmAdapter(config);
92
98
 
99
+ // Resolve download policy from env/flags
100
+ const globals = getGlobals();
101
+ const policy = resolveDownloadPolicy(process.env, {
102
+ offline: globals.offline,
103
+ });
104
+
105
+ // Create progress renderer for model downloads (throttled)
106
+ const showProgress = !options.json && process.stderr.isTTY;
107
+ const downloadProgress = showProgress
108
+ ? createThrottledProgressRenderer(createProgressRenderer())
109
+ : undefined;
110
+
93
111
  // Create embedding port (for vector search)
94
112
  const embedUri = options.embedModel ?? preset.embed;
95
- const embedResult = await llm.createEmbeddingPort(embedUri);
113
+ const embedResult = await llm.createEmbeddingPort(embedUri, {
114
+ policy,
115
+ onProgress: downloadProgress
116
+ ? (progress) => downloadProgress('embed', progress)
117
+ : undefined,
118
+ });
96
119
  if (embedResult.ok) {
97
120
  embedPort = embedResult.value;
98
121
  }
@@ -100,7 +123,12 @@ export async function query(
100
123
  // Create generation port (for expansion) - optional
101
124
  if (!options.noExpand) {
102
125
  const genUri = options.genModel ?? preset.gen;
103
- const genResult = await llm.createGenerationPort(genUri);
126
+ const genResult = await llm.createGenerationPort(genUri, {
127
+ policy,
128
+ onProgress: downloadProgress
129
+ ? (progress) => downloadProgress('gen', progress)
130
+ : undefined,
131
+ });
104
132
  if (genResult.ok) {
105
133
  genPort = genResult.value;
106
134
  }
@@ -109,12 +137,22 @@ export async function query(
109
137
  // Create rerank port - optional
110
138
  if (!options.noRerank) {
111
139
  const rerankUri = options.rerankModel ?? preset.rerank;
112
- const rerankResult = await llm.createRerankPort(rerankUri);
140
+ const rerankResult = await llm.createRerankPort(rerankUri, {
141
+ policy,
142
+ onProgress: downloadProgress
143
+ ? (progress) => downloadProgress('rerank', progress)
144
+ : undefined,
145
+ });
113
146
  if (rerankResult.ok) {
114
147
  rerankPort = rerankResult.value;
115
148
  }
116
149
  }
117
150
 
151
+ // Clear progress line if shown
152
+ if (showProgress && downloadProgress) {
153
+ process.stderr.write('\n');
154
+ }
155
+
118
156
  // Create vector index (optional)
119
157
  let vectorIndex: VectorIndexPort | null = null;
120
158
  if (embedPort) {