@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 +9 -1
- package/assets/screenshots/claudecodeskill.jpg +0 -0
- package/assets/screenshots/cli.jpg +0 -0
- package/assets/screenshots/mcp.jpg +0 -0
- package/assets/screenshots/webui-ask-answer.jpg +0 -0
- package/assets/screenshots/webui-home.jpg +0 -0
- package/package.json +4 -4
- package/src/cli/commands/ask.ts +41 -3
- package/src/cli/commands/collection/add.ts +27 -66
- package/src/cli/commands/collection/remove.ts +20 -39
- package/src/cli/commands/embed.ts +75 -20
- package/src/cli/commands/models/index.ts +1 -1
- package/src/cli/commands/models/pull.ts +0 -17
- package/src/cli/commands/query.ts +41 -3
- package/src/cli/context.ts +10 -0
- package/src/cli/program.ts +2 -1
- package/src/cli/progress.ts +88 -0
- package/src/cli/run.ts +1 -0
- package/src/collection/add.ts +113 -0
- package/src/collection/index.ts +17 -0
- package/src/collection/remove.ts +65 -0
- package/src/collection/types.ts +70 -0
- package/src/llm/cache.ts +187 -37
- package/src/llm/errors.ts +27 -4
- package/src/llm/lockfile.ts +216 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +54 -12
- package/src/llm/policy.ts +84 -0
- package/src/mcp/tools/query.ts +20 -3
- package/src/mcp/tools/vsearch.ts +12 -1
- package/src/serve/config-sync.ts +139 -0
- package/src/serve/context.ts +36 -3
- package/src/serve/jobs.ts +172 -0
- package/src/serve/routes/api.ts +432 -0
- package/src/serve/security.ts +84 -0
- package/src/serve/server.ts +126 -15
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gmickel/gno",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
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": "
|
|
120
|
+
"ultracite": "7.0.4"
|
|
121
121
|
},
|
|
122
122
|
"peerDependencies": {
|
|
123
123
|
"typescript": "^5"
|
package/src/cli/commands/ask.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
47
|
-
if (!
|
|
39
|
+
const configResult = await loadConfig();
|
|
40
|
+
if (!configResult.ok) {
|
|
48
41
|
throw new CliError(
|
|
49
42
|
'RUNTIME',
|
|
50
|
-
`Failed to load config: ${
|
|
43
|
+
`Failed to load config: ${configResult.error.message}`
|
|
51
44
|
);
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
'
|
|
102
|
-
|
|
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(
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
17
|
-
if (!
|
|
11
|
+
const configResult = await loadConfig();
|
|
12
|
+
if (!configResult.ok) {
|
|
18
13
|
throw new CliError(
|
|
19
14
|
'RUNTIME',
|
|
20
|
-
`Failed to load config: ${
|
|
15
|
+
`Failed to load config: ${configResult.error.message}`
|
|
21
16
|
);
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
//
|
|
205
|
+
// Helpers
|
|
195
206
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
196
207
|
|
|
208
|
+
interface EmbedContext {
|
|
209
|
+
config: Config;
|
|
210
|
+
modelUri: string;
|
|
211
|
+
store: SqliteAdapter;
|
|
212
|
+
}
|
|
213
|
+
|
|
197
214
|
/**
|
|
198
|
-
*
|
|
215
|
+
* Initialize embed context: check init, load config, open store.
|
|
199
216
|
*/
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 {
|
|
223
|
+
return { ok: false, error: 'GNO not initialized. Run: gno init' };
|
|
209
224
|
}
|
|
210
225
|
|
|
211
|
-
|
|
212
|
-
const configResult = await loadConfig(options.configPath);
|
|
226
|
+
const configResult = await loadConfig(configPath);
|
|
213
227
|
if (!configResult.ok) {
|
|
214
|
-
return {
|
|
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 =
|
|
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 {
|
|
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) {
|