@gmickel/gno 0.6.0 → 0.6.1

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.6.1",
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",
@@ -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) {
@@ -9,6 +9,7 @@ import type { Database } from 'bun:sqlite';
9
9
  import { getIndexDbPath } from '../../app/constants';
10
10
  import { getConfigPaths, isInitialized, loadConfig } from '../../config';
11
11
  import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
12
+ import { resolveDownloadPolicy } from '../../llm/policy';
12
13
  import { getActivePreset } from '../../llm/registry';
13
14
  import type { EmbeddingPort } from '../../llm/types';
14
15
  import { formatDocForEmbedding } from '../../pipeline/contextual';
@@ -23,6 +24,11 @@ import {
23
24
  type VectorRow,
24
25
  type VectorStatsPort,
25
26
  } from '../../store/vector';
27
+ import { getGlobals } from '../program';
28
+ import {
29
+ createProgressRenderer,
30
+ createThrottledProgressRenderer,
31
+ } from '../progress';
26
32
 
27
33
  // ─────────────────────────────────────────────────────────────────────────────
28
34
  // Types
@@ -274,14 +280,35 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
274
280
  };
275
281
  }
276
282
 
277
- // Create LLM adapter and embedding port
283
+ // Create LLM adapter and embedding port with auto-download
284
+ const globals = getGlobals();
285
+ const policy = resolveDownloadPolicy(process.env, {
286
+ offline: globals.offline,
287
+ });
288
+
289
+ // Create progress renderer for model download (throttled to avoid spam)
290
+ const showDownloadProgress = !options.json && process.stderr.isTTY;
291
+ const downloadProgress = showDownloadProgress
292
+ ? createThrottledProgressRenderer(createProgressRenderer())
293
+ : undefined;
294
+
278
295
  const llm = new LlmAdapter(config);
279
- const embedResult = await llm.createEmbeddingPort(modelUri);
296
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
297
+ policy,
298
+ onProgress: downloadProgress
299
+ ? (progress) => downloadProgress('embed', progress)
300
+ : undefined,
301
+ });
280
302
  if (!embedResult.ok) {
281
303
  return { success: false, error: embedResult.error.message };
282
304
  }
283
305
  embedPort = embedResult.value;
284
306
 
307
+ // Clear download progress line if shown
308
+ if (showDownloadProgress) {
309
+ process.stderr.write('\n');
310
+ }
311
+
285
312
  // Discover dimensions via probe embedding
286
313
  const probeResult = await embedPort.embed('dimension probe');
287
314
  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) {
@@ -5,6 +5,7 @@
5
5
  * @module src/cli/context
6
6
  */
7
7
 
8
+ import { envIsSet } from '../llm/policy';
8
9
  import { setColorsEnabled } from './colors';
9
10
 
10
11
  // ─────────────────────────────────────────────────────────────────────────────
@@ -19,6 +20,7 @@ export interface GlobalOptions {
19
20
  yes: boolean;
20
21
  quiet: boolean;
21
22
  json: boolean;
23
+ offline: boolean;
22
24
  }
23
25
 
24
26
  // ─────────────────────────────────────────────────────────────────────────────
@@ -40,6 +42,13 @@ export function parseGlobalOptions(
40
42
 
41
43
  const colorEnabled = !(noColorEnv || noColorFlag);
42
44
 
45
+ // Offline mode: --offline flag or HF_HUB_OFFLINE/GNO_OFFLINE env var
46
+ // Use envIsSet for consistent truthiness (treats "1", "true", "yes" as true)
47
+ const offlineEnv =
48
+ envIsSet(env, 'HF_HUB_OFFLINE') || envIsSet(env, 'GNO_OFFLINE');
49
+ const offlineFlag = Boolean(raw.offline);
50
+ const offlineEnabled = offlineEnv || offlineFlag;
51
+
43
52
  return {
44
53
  index: (raw.index as string) ?? 'default',
45
54
  config: raw.config as string | undefined,
@@ -48,6 +57,7 @@ export function parseGlobalOptions(
48
57
  yes: Boolean(raw.yes),
49
58
  quiet: Boolean(raw.quiet),
50
59
  json: Boolean(raw.json),
60
+ offline: offlineEnabled,
51
61
  };
52
62
  }
53
63
 
@@ -132,7 +132,8 @@ export function createProgram(): Command {
132
132
  .option('--verbose', 'verbose logging')
133
133
  .option('--yes', 'non-interactive mode')
134
134
  .option('-q, --quiet', 'suppress non-essential output')
135
- .option('--json', 'JSON output (for errors and supported commands)');
135
+ .option('--json', 'JSON output (for errors and supported commands)')
136
+ .option('--offline', 'offline mode (use cached models only)');
136
137
 
137
138
  // Resolve globals ONCE before any command runs (ensures consistency)
138
139
  program.hook('preAction', (thisCommand) => {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Progress rendering utilities for CLI.
3
+ * Kept in CLI layer to avoid layer violations.
4
+ *
5
+ * @module src/cli/progress
6
+ */
7
+
8
+ import type { DownloadProgress, ModelType } from '../llm/types';
9
+
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+ // Types
12
+ // ─────────────────────────────────────────────────────────────────────────────
13
+
14
+ export type ProgressCallback<T = ModelType> = (
15
+ type: T,
16
+ progress: DownloadProgress
17
+ ) => void;
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+ // Progress Renderers
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Create a terminal progress renderer for model downloads.
25
+ * Writes progress to stderr with carriage return for in-place updates.
26
+ */
27
+ export function createProgressRenderer(): ProgressCallback {
28
+ return (type, progress) => {
29
+ const percent = progress.percent.toFixed(1);
30
+ const downloaded = (progress.downloadedBytes / 1024 / 1024).toFixed(1);
31
+ const total = (progress.totalBytes / 1024 / 1024).toFixed(1);
32
+ process.stderr.write(
33
+ `\r${type}: ${percent}% (${downloaded}/${total} MB) `
34
+ );
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Create a throttled progress renderer.
40
+ * Emits at most once per interval, plus always on completion.
41
+ *
42
+ * @param renderer - Underlying renderer to throttle
43
+ * @param intervalMs - Minimum interval between emissions (default: 100ms)
44
+ */
45
+ export function createThrottledProgressRenderer(
46
+ renderer: ProgressCallback,
47
+ intervalMs = 100
48
+ ): ProgressCallback {
49
+ let lastEmit = 0;
50
+
51
+ return (type, progress) => {
52
+ const now = Date.now();
53
+
54
+ // Always emit on completion (100%) or error
55
+ const isComplete = progress.percent >= 100;
56
+
57
+ // Emit if enough time passed or completing
58
+ if (isComplete || now - lastEmit >= intervalMs) {
59
+ renderer(type, progress);
60
+ lastEmit = now;
61
+ }
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Create a non-TTY progress renderer (periodic line output).
67
+ * For non-interactive contexts like CI or logs.
68
+ */
69
+ export function createNonTtyProgressRenderer(
70
+ intervalMs = 5000
71
+ ): ProgressCallback {
72
+ let lastEmit = 0;
73
+
74
+ return (type, progress) => {
75
+ const now = Date.now();
76
+ const isComplete = progress.percent >= 100;
77
+
78
+ if (isComplete || now - lastEmit >= intervalMs) {
79
+ const percent = progress.percent.toFixed(1);
80
+ const downloaded = (progress.downloadedBytes / 1024 / 1024).toFixed(1);
81
+ const total = (progress.totalBytes / 1024 / 1024).toFixed(1);
82
+ process.stderr.write(
83
+ `${type}: ${percent}% (${downloaded}/${total} MB)\n`
84
+ );
85
+ lastEmit = now;
86
+ }
87
+ };
88
+ }
package/src/cli/run.ts CHANGED
@@ -35,6 +35,7 @@ const KNOWN_BOOL_FLAGS = new Set([
35
35
  '-q',
36
36
  '--quiet',
37
37
  '--json',
38
+ '--offline',
38
39
  ]);
39
40
 
40
41
  // Known global flags that take values (--flag value or --flag=value)