@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 +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 +1 -1
- package/src/cli/commands/ask.ts +41 -3
- package/src/cli/commands/embed.ts +29 -2
- 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/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/context.ts +36 -3
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download policy resolution.
|
|
3
|
+
* Determines whether model downloads are allowed based on env/flags.
|
|
4
|
+
*
|
|
5
|
+
* @module src/llm/policy
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
9
|
+
// Types
|
|
10
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface DownloadPolicy {
|
|
13
|
+
/** True if network is disabled (no HF API calls at all) */
|
|
14
|
+
offline: boolean;
|
|
15
|
+
/** True if auto-download is allowed (may still be blocked by offline) */
|
|
16
|
+
allowDownload: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PolicyFlags {
|
|
20
|
+
/** --offline CLI flag */
|
|
21
|
+
offline?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Helpers
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if env var is set (non-empty and truthy).
|
|
30
|
+
* Treats "1", "true", "yes" as truthy. Empty string or "0" as falsy.
|
|
31
|
+
*/
|
|
32
|
+
export function envIsSet(
|
|
33
|
+
env: Record<string, string | undefined>,
|
|
34
|
+
key: string
|
|
35
|
+
): boolean {
|
|
36
|
+
const val = env[key];
|
|
37
|
+
if (val === undefined || val === '') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const lower = val.toLowerCase();
|
|
41
|
+
return lower === '1' || lower === 'true' || lower === 'yes';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Main
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolve download policy from environment and CLI flags.
|
|
50
|
+
*
|
|
51
|
+
* Precedence (first wins):
|
|
52
|
+
* 1. --offline flag → offline=true, allowDownload=false
|
|
53
|
+
* 2. HF_HUB_OFFLINE=1 → offline=true, allowDownload=false
|
|
54
|
+
* 3. GNO_OFFLINE=1 → offline=true, allowDownload=false
|
|
55
|
+
* 4. GNO_NO_AUTO_DOWNLOAD=1 → offline=false, allowDownload=false
|
|
56
|
+
* 5. Default → offline=false, allowDownload=true
|
|
57
|
+
*/
|
|
58
|
+
export function resolveDownloadPolicy(
|
|
59
|
+
env: Record<string, string | undefined>,
|
|
60
|
+
flags: PolicyFlags
|
|
61
|
+
): DownloadPolicy {
|
|
62
|
+
// 1. --offline flag takes highest precedence
|
|
63
|
+
if (flags.offline) {
|
|
64
|
+
return { offline: true, allowDownload: false };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. HF_HUB_OFFLINE env var (standard HuggingFace offline mode)
|
|
68
|
+
if (envIsSet(env, 'HF_HUB_OFFLINE')) {
|
|
69
|
+
return { offline: true, allowDownload: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. GNO_OFFLINE env var (GNO-specific offline mode)
|
|
73
|
+
if (envIsSet(env, 'GNO_OFFLINE')) {
|
|
74
|
+
return { offline: true, allowDownload: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. GNO_NO_AUTO_DOWNLOAD env var (allow resolve but no download)
|
|
78
|
+
if (envIsSet(env, 'GNO_NO_AUTO_DOWNLOAD')) {
|
|
79
|
+
return { offline: false, allowDownload: false };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 5. Default: allow downloads
|
|
83
|
+
return { offline: false, allowDownload: true };
|
|
84
|
+
}
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { join as pathJoin } from 'node:path';
|
|
8
8
|
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { createNonTtyProgressRenderer } from '../../cli/progress';
|
|
9
10
|
import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
|
|
11
|
+
import { resolveDownloadPolicy } from '../../llm/policy';
|
|
10
12
|
import { getActivePreset } from '../../llm/registry';
|
|
11
13
|
import type {
|
|
12
14
|
EmbeddingPort,
|
|
@@ -128,6 +130,12 @@ export function handleQuery(
|
|
|
128
130
|
const preset = getActivePreset(ctx.config);
|
|
129
131
|
const llm = new LlmAdapter(ctx.config);
|
|
130
132
|
|
|
133
|
+
// Resolve download policy from env (MCP has no CLI flags)
|
|
134
|
+
const policy = resolveDownloadPolicy(process.env, {});
|
|
135
|
+
|
|
136
|
+
// Non-TTY progress for MCP (periodic lines to stderr, not \r)
|
|
137
|
+
const downloadProgress = createNonTtyProgressRenderer();
|
|
138
|
+
|
|
131
139
|
let embedPort: EmbeddingPort | null = null;
|
|
132
140
|
let genPort: GenerationPort | null = null;
|
|
133
141
|
let rerankPort: RerankPort | null = null;
|
|
@@ -135,7 +143,10 @@ export function handleQuery(
|
|
|
135
143
|
|
|
136
144
|
try {
|
|
137
145
|
// Create embedding port (for vector search) - optional
|
|
138
|
-
const embedResult = await llm.createEmbeddingPort(preset.embed
|
|
146
|
+
const embedResult = await llm.createEmbeddingPort(preset.embed, {
|
|
147
|
+
policy,
|
|
148
|
+
onProgress: (progress) => downloadProgress('embed', progress),
|
|
149
|
+
});
|
|
139
150
|
if (embedResult.ok) {
|
|
140
151
|
embedPort = embedResult.value;
|
|
141
152
|
}
|
|
@@ -164,7 +175,10 @@ export function handleQuery(
|
|
|
164
175
|
|
|
165
176
|
// Create generation port (for expansion) - optional
|
|
166
177
|
if (!noExpand) {
|
|
167
|
-
const genResult = await llm.createGenerationPort(preset.gen
|
|
178
|
+
const genResult = await llm.createGenerationPort(preset.gen, {
|
|
179
|
+
policy,
|
|
180
|
+
onProgress: (progress) => downloadProgress('gen', progress),
|
|
181
|
+
});
|
|
168
182
|
if (genResult.ok) {
|
|
169
183
|
genPort = genResult.value;
|
|
170
184
|
}
|
|
@@ -172,7 +186,10 @@ export function handleQuery(
|
|
|
172
186
|
|
|
173
187
|
// Create rerank port - optional
|
|
174
188
|
if (!noRerank) {
|
|
175
|
-
const rerankResult = await llm.createRerankPort(preset.rerank
|
|
189
|
+
const rerankResult = await llm.createRerankPort(preset.rerank, {
|
|
190
|
+
policy,
|
|
191
|
+
onProgress: (progress) => downloadProgress('rerank', progress),
|
|
192
|
+
});
|
|
176
193
|
if (rerankResult.ok) {
|
|
177
194
|
rerankPort = rerankResult.value;
|
|
178
195
|
}
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { join as pathJoin } from 'node:path';
|
|
8
8
|
import { parseUri } from '../../app/constants';
|
|
9
|
+
import { createNonTtyProgressRenderer } from '../../cli/progress';
|
|
9
10
|
import { LlmAdapter } from '../../llm/nodeLlamaCpp/adapter';
|
|
11
|
+
import { resolveDownloadPolicy } from '../../llm/policy';
|
|
10
12
|
import { getActivePreset } from '../../llm/registry';
|
|
11
13
|
import { formatQueryForEmbedding } from '../../pipeline/contextual';
|
|
12
14
|
import type { SearchResult, SearchResults } from '../../pipeline/types';
|
|
@@ -109,9 +111,18 @@ export function handleVsearch(
|
|
|
109
111
|
const preset = getActivePreset(ctx.config);
|
|
110
112
|
const modelUri = preset.embed;
|
|
111
113
|
|
|
114
|
+
// Resolve download policy from env (MCP has no CLI flags)
|
|
115
|
+
const policy = resolveDownloadPolicy(process.env, {});
|
|
116
|
+
|
|
117
|
+
// Non-TTY progress for MCP (periodic lines to stderr, not \r)
|
|
118
|
+
const downloadProgress = createNonTtyProgressRenderer();
|
|
119
|
+
|
|
112
120
|
// Create LLM adapter for embeddings
|
|
113
121
|
const llm = new LlmAdapter(ctx.config);
|
|
114
|
-
const embedResult = await llm.createEmbeddingPort(modelUri
|
|
122
|
+
const embedResult = await llm.createEmbeddingPort(modelUri, {
|
|
123
|
+
policy,
|
|
124
|
+
onProgress: (progress) => downloadProgress('embed', progress),
|
|
125
|
+
});
|
|
115
126
|
if (!embedResult.ok) {
|
|
116
127
|
throw new Error(
|
|
117
128
|
`Failed to load embedding model: ${embedResult.error.message}. ` +
|
package/src/serve/context.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Config } from '../config/types';
|
|
9
|
+
import type { CreatePortOptions } from '../llm/nodeLlamaCpp/adapter';
|
|
9
10
|
import { LlmAdapter } from '../llm/nodeLlamaCpp/adapter';
|
|
11
|
+
import { resolveDownloadPolicy } from '../llm/policy';
|
|
10
12
|
import { getActivePreset } from '../llm/registry';
|
|
11
13
|
import type {
|
|
12
14
|
DownloadProgress,
|
|
@@ -87,8 +89,27 @@ export async function createServerContext(
|
|
|
87
89
|
const preset = getActivePreset(config);
|
|
88
90
|
const llm = new LlmAdapter(config);
|
|
89
91
|
|
|
92
|
+
// Resolve download policy from env (serve has no CLI flags)
|
|
93
|
+
const policy = resolveDownloadPolicy(process.env, {});
|
|
94
|
+
|
|
95
|
+
// Progress callback updates downloadState for WebUI polling
|
|
96
|
+
const createPortOptions = (type: ModelType): CreatePortOptions => ({
|
|
97
|
+
policy,
|
|
98
|
+
onProgress: (progress) => {
|
|
99
|
+
downloadState.active = true;
|
|
100
|
+
downloadState.currentType = type;
|
|
101
|
+
downloadState.progress = progress;
|
|
102
|
+
if (progress.percent >= 100) {
|
|
103
|
+
downloadState.completed.push(type);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
90
108
|
// Try to create embedding port
|
|
91
|
-
const embedResult = await llm.createEmbeddingPort(
|
|
109
|
+
const embedResult = await llm.createEmbeddingPort(
|
|
110
|
+
preset.embed,
|
|
111
|
+
createPortOptions('embed')
|
|
112
|
+
);
|
|
92
113
|
if (embedResult.ok) {
|
|
93
114
|
embedPort = embedResult.value;
|
|
94
115
|
const initResult = await embedPort.init();
|
|
@@ -108,18 +129,30 @@ export async function createServerContext(
|
|
|
108
129
|
}
|
|
109
130
|
|
|
110
131
|
// Try to create generation port
|
|
111
|
-
const genResult = await llm.createGenerationPort(
|
|
132
|
+
const genResult = await llm.createGenerationPort(
|
|
133
|
+
preset.gen,
|
|
134
|
+
createPortOptions('gen')
|
|
135
|
+
);
|
|
112
136
|
if (genResult.ok) {
|
|
113
137
|
genPort = genResult.value;
|
|
114
138
|
console.log('AI answer generation enabled');
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
// Try to create rerank port
|
|
118
|
-
const rerankResult = await llm.createRerankPort(
|
|
142
|
+
const rerankResult = await llm.createRerankPort(
|
|
143
|
+
preset.rerank,
|
|
144
|
+
createPortOptions('rerank')
|
|
145
|
+
);
|
|
119
146
|
if (rerankResult.ok) {
|
|
120
147
|
rerankPort = rerankResult.value;
|
|
121
148
|
console.log('Reranking enabled');
|
|
122
149
|
}
|
|
150
|
+
|
|
151
|
+
// Reset download state after initialization
|
|
152
|
+
if (downloadState.active) {
|
|
153
|
+
downloadState.active = false;
|
|
154
|
+
downloadState.currentType = null;
|
|
155
|
+
}
|
|
123
156
|
} catch (e) {
|
|
124
157
|
// Log but don't fail - models are optional
|
|
125
158
|
console.log(
|