@gmickel/gno 0.3.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.
Files changed (131) hide show
  1. package/README.md +256 -0
  2. package/assets/skill/SKILL.md +112 -0
  3. package/assets/skill/cli-reference.md +327 -0
  4. package/assets/skill/examples.md +234 -0
  5. package/assets/skill/mcp-reference.md +159 -0
  6. package/package.json +90 -0
  7. package/src/app/constants.ts +313 -0
  8. package/src/cli/colors.ts +65 -0
  9. package/src/cli/commands/ask.ts +545 -0
  10. package/src/cli/commands/cleanup.ts +105 -0
  11. package/src/cli/commands/collection/add.ts +120 -0
  12. package/src/cli/commands/collection/index.ts +10 -0
  13. package/src/cli/commands/collection/list.ts +108 -0
  14. package/src/cli/commands/collection/remove.ts +64 -0
  15. package/src/cli/commands/collection/rename.ts +95 -0
  16. package/src/cli/commands/context/add.ts +67 -0
  17. package/src/cli/commands/context/check.ts +153 -0
  18. package/src/cli/commands/context/index.ts +10 -0
  19. package/src/cli/commands/context/list.ts +109 -0
  20. package/src/cli/commands/context/rm.ts +52 -0
  21. package/src/cli/commands/doctor.ts +393 -0
  22. package/src/cli/commands/embed.ts +462 -0
  23. package/src/cli/commands/get.ts +356 -0
  24. package/src/cli/commands/index-cmd.ts +119 -0
  25. package/src/cli/commands/index.ts +102 -0
  26. package/src/cli/commands/init.ts +328 -0
  27. package/src/cli/commands/ls.ts +217 -0
  28. package/src/cli/commands/mcp/config.ts +300 -0
  29. package/src/cli/commands/mcp/index.ts +24 -0
  30. package/src/cli/commands/mcp/install.ts +203 -0
  31. package/src/cli/commands/mcp/paths.ts +470 -0
  32. package/src/cli/commands/mcp/status.ts +222 -0
  33. package/src/cli/commands/mcp/uninstall.ts +158 -0
  34. package/src/cli/commands/mcp.ts +20 -0
  35. package/src/cli/commands/models/clear.ts +103 -0
  36. package/src/cli/commands/models/index.ts +32 -0
  37. package/src/cli/commands/models/list.ts +214 -0
  38. package/src/cli/commands/models/path.ts +51 -0
  39. package/src/cli/commands/models/pull.ts +199 -0
  40. package/src/cli/commands/models/use.ts +85 -0
  41. package/src/cli/commands/multi-get.ts +400 -0
  42. package/src/cli/commands/query.ts +220 -0
  43. package/src/cli/commands/ref-parser.ts +108 -0
  44. package/src/cli/commands/reset.ts +191 -0
  45. package/src/cli/commands/search.ts +136 -0
  46. package/src/cli/commands/shared.ts +156 -0
  47. package/src/cli/commands/skill/index.ts +19 -0
  48. package/src/cli/commands/skill/install.ts +197 -0
  49. package/src/cli/commands/skill/paths-cmd.ts +81 -0
  50. package/src/cli/commands/skill/paths.ts +191 -0
  51. package/src/cli/commands/skill/show.ts +73 -0
  52. package/src/cli/commands/skill/uninstall.ts +141 -0
  53. package/src/cli/commands/status.ts +205 -0
  54. package/src/cli/commands/update.ts +68 -0
  55. package/src/cli/commands/vsearch.ts +188 -0
  56. package/src/cli/context.ts +64 -0
  57. package/src/cli/errors.ts +64 -0
  58. package/src/cli/format/search-results.ts +211 -0
  59. package/src/cli/options.ts +183 -0
  60. package/src/cli/program.ts +1330 -0
  61. package/src/cli/run.ts +213 -0
  62. package/src/cli/ui.ts +92 -0
  63. package/src/config/defaults.ts +20 -0
  64. package/src/config/index.ts +55 -0
  65. package/src/config/loader.ts +161 -0
  66. package/src/config/paths.ts +87 -0
  67. package/src/config/saver.ts +153 -0
  68. package/src/config/types.ts +280 -0
  69. package/src/converters/adapters/markitdownTs/adapter.ts +140 -0
  70. package/src/converters/adapters/officeparser/adapter.ts +126 -0
  71. package/src/converters/canonicalize.ts +89 -0
  72. package/src/converters/errors.ts +218 -0
  73. package/src/converters/index.ts +51 -0
  74. package/src/converters/mime.ts +163 -0
  75. package/src/converters/native/markdown.ts +115 -0
  76. package/src/converters/native/plaintext.ts +56 -0
  77. package/src/converters/path.ts +48 -0
  78. package/src/converters/pipeline.ts +159 -0
  79. package/src/converters/registry.ts +74 -0
  80. package/src/converters/types.ts +123 -0
  81. package/src/converters/versions.ts +24 -0
  82. package/src/index.ts +27 -0
  83. package/src/ingestion/chunker.ts +238 -0
  84. package/src/ingestion/index.ts +32 -0
  85. package/src/ingestion/language.ts +276 -0
  86. package/src/ingestion/sync.ts +671 -0
  87. package/src/ingestion/types.ts +219 -0
  88. package/src/ingestion/walker.ts +235 -0
  89. package/src/llm/cache.ts +467 -0
  90. package/src/llm/errors.ts +191 -0
  91. package/src/llm/index.ts +58 -0
  92. package/src/llm/nodeLlamaCpp/adapter.ts +133 -0
  93. package/src/llm/nodeLlamaCpp/embedding.ts +165 -0
  94. package/src/llm/nodeLlamaCpp/generation.ts +88 -0
  95. package/src/llm/nodeLlamaCpp/lifecycle.ts +317 -0
  96. package/src/llm/nodeLlamaCpp/rerank.ts +94 -0
  97. package/src/llm/registry.ts +86 -0
  98. package/src/llm/types.ts +129 -0
  99. package/src/mcp/resources/index.ts +151 -0
  100. package/src/mcp/server.ts +229 -0
  101. package/src/mcp/tools/get.ts +220 -0
  102. package/src/mcp/tools/index.ts +160 -0
  103. package/src/mcp/tools/multi-get.ts +263 -0
  104. package/src/mcp/tools/query.ts +226 -0
  105. package/src/mcp/tools/search.ts +119 -0
  106. package/src/mcp/tools/status.ts +81 -0
  107. package/src/mcp/tools/vsearch.ts +198 -0
  108. package/src/pipeline/chunk-lookup.ts +44 -0
  109. package/src/pipeline/expansion.ts +256 -0
  110. package/src/pipeline/explain.ts +115 -0
  111. package/src/pipeline/fusion.ts +185 -0
  112. package/src/pipeline/hybrid.ts +535 -0
  113. package/src/pipeline/index.ts +64 -0
  114. package/src/pipeline/query-language.ts +118 -0
  115. package/src/pipeline/rerank.ts +223 -0
  116. package/src/pipeline/search.ts +261 -0
  117. package/src/pipeline/types.ts +328 -0
  118. package/src/pipeline/vsearch.ts +348 -0
  119. package/src/store/index.ts +41 -0
  120. package/src/store/migrations/001-initial.ts +196 -0
  121. package/src/store/migrations/index.ts +20 -0
  122. package/src/store/migrations/runner.ts +187 -0
  123. package/src/store/sqlite/adapter.ts +1242 -0
  124. package/src/store/sqlite/index.ts +7 -0
  125. package/src/store/sqlite/setup.ts +129 -0
  126. package/src/store/sqlite/types.ts +28 -0
  127. package/src/store/types.ts +506 -0
  128. package/src/store/vector/index.ts +13 -0
  129. package/src/store/vector/sqlite-vec.ts +373 -0
  130. package/src/store/vector/stats.ts +152 -0
  131. package/src/store/vector/types.ts +115 -0
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Model cache resolver.
3
+ * Handles hf: URI parsing and model cache management.
4
+ *
5
+ * @module src/llm/cache
6
+ */
7
+
8
+ import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
9
+ // node:path: join for path construction, isAbsolute for cross-platform path detection
10
+ import { isAbsolute, join } from 'node:path';
11
+ // node:url: fileURLToPath for proper file:// URL handling
12
+ import { fileURLToPath } from 'node:url';
13
+ import { getModelsCachePath } from '../app/constants';
14
+ import {
15
+ downloadFailedError,
16
+ invalidUriError,
17
+ modelNotCachedError,
18
+ modelNotFoundError,
19
+ } from './errors';
20
+ import type {
21
+ DownloadProgress,
22
+ LlmResult,
23
+ ModelCacheEntry,
24
+ ModelType,
25
+ ProgressCallback,
26
+ } from './types';
27
+
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+ // URI Parsing
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+
32
+ // Regex patterns for URI parsing (top-level for performance)
33
+ const HF_QUANT_PATTERN = /^([^/]+)\/([^/:]+):(\w+)$/;
34
+ const HF_PATH_PATTERN = /^([^/]+)\/([^/]+)\/(.+\.gguf)$/;
35
+
36
+ export type ParsedModelUri =
37
+ | {
38
+ scheme: 'hf';
39
+ org: string;
40
+ repo: string;
41
+ file: string;
42
+ quantization?: string;
43
+ }
44
+ | {
45
+ scheme: 'file';
46
+ file: string;
47
+ };
48
+
49
+ /**
50
+ * Parse a model URI into components.
51
+ *
52
+ * Supported formats:
53
+ * - hf:org/repo/file.gguf (explicit file)
54
+ * - hf:org/repo:Q4_K_M (quantization shorthand - infers filename)
55
+ * - file:///path/to/model.gguf (standard file URL)
56
+ * - file:///C:/path/to/model.gguf (Windows file URL)
57
+ * - file:/path/to/model.gguf (simplified file URI)
58
+ * - /path/to/model.gguf (Unix absolute path)
59
+ * - C:\path\to\model.gguf (Windows absolute path)
60
+ * - \\server\share\model.gguf (UNC path)
61
+ */
62
+ export function parseModelUri(
63
+ uri: string
64
+ ): { ok: true; value: ParsedModelUri } | { ok: false; error: string } {
65
+ // Handle hf: scheme
66
+ if (uri.startsWith('hf:')) {
67
+ const rest = uri.slice(3);
68
+
69
+ // Check for quantization shorthand: hf:org/repo:Q4_K_M
70
+ const colonMatch = rest.match(HF_QUANT_PATTERN);
71
+ if (colonMatch) {
72
+ const [, org, repo, quant] = colonMatch;
73
+ // Regex guarantees these are defined when match succeeds
74
+ if (org && repo && quant) {
75
+ return {
76
+ ok: true,
77
+ value: {
78
+ scheme: 'hf',
79
+ org,
80
+ repo,
81
+ file: '', // Will be resolved by node-llama-cpp
82
+ quantization: quant,
83
+ },
84
+ };
85
+ }
86
+ }
87
+
88
+ // Full path: hf:org/repo/file.gguf
89
+ const pathMatch = rest.match(HF_PATH_PATTERN);
90
+ if (pathMatch) {
91
+ const [, org, repo, file] = pathMatch;
92
+ // Regex guarantees these are defined when match succeeds
93
+ if (org && repo && file) {
94
+ return {
95
+ ok: true,
96
+ value: {
97
+ scheme: 'hf',
98
+ org,
99
+ repo,
100
+ file,
101
+ },
102
+ };
103
+ }
104
+ }
105
+
106
+ return { ok: false, error: `Invalid hf: URI format: ${uri}` };
107
+ }
108
+
109
+ // Handle file:// URLs (proper file URLs like file:///C:/path or file:///path)
110
+ if (uri.startsWith('file://')) {
111
+ try {
112
+ const filePath = fileURLToPath(new URL(uri));
113
+ return {
114
+ ok: true,
115
+ value: { scheme: 'file', file: filePath },
116
+ };
117
+ } catch {
118
+ return { ok: false, error: `Invalid file URL: ${uri}` };
119
+ }
120
+ }
121
+
122
+ // Handle simplified file: scheme (file:/path or file:C:\path)
123
+ if (uri.startsWith('file:')) {
124
+ const path = uri.slice(5);
125
+ if (!path) {
126
+ return { ok: false, error: 'Empty file path' };
127
+ }
128
+ return {
129
+ ok: true,
130
+ value: { scheme: 'file', file: path },
131
+ };
132
+ }
133
+
134
+ // Treat as local file path if absolute (works on both Unix and Windows)
135
+ if (isAbsolute(uri)) {
136
+ return {
137
+ ok: true,
138
+ value: { scheme: 'file', file: uri },
139
+ };
140
+ }
141
+
142
+ return { ok: false, error: `Unknown URI scheme: ${uri}` };
143
+ }
144
+
145
+ /**
146
+ * Convert parsed URI back to node-llama-cpp format.
147
+ */
148
+ export function toNodeLlamaCppUri(parsed: ParsedModelUri): string {
149
+ if (parsed.scheme === 'file') {
150
+ return parsed.file;
151
+ }
152
+
153
+ // hf: format for node-llama-cpp
154
+ if (parsed.quantization) {
155
+ return `hf:${parsed.org}/${parsed.repo}:${parsed.quantization}`;
156
+ }
157
+ return `hf:${parsed.org}/${parsed.repo}/${parsed.file}`;
158
+ }
159
+
160
+ // ─────────────────────────────────────────────────────────────────────────────
161
+ // Manifest
162
+ // ─────────────────────────────────────────────────────────────────────────────
163
+
164
+ interface Manifest {
165
+ version: '1.0';
166
+ models: ModelCacheEntry[];
167
+ }
168
+
169
+ const MANIFEST_VERSION = '1.0' as const;
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // ModelCache
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ export class ModelCache {
176
+ readonly dir: string;
177
+ private readonly manifestPath: string;
178
+ private manifest: Manifest | null = null;
179
+
180
+ constructor(cacheDir?: string) {
181
+ this.dir = cacheDir ?? getModelsCachePath();
182
+ this.manifestPath = join(this.dir, 'manifest.json');
183
+ }
184
+
185
+ /**
186
+ * Resolve a model URI to a local file path.
187
+ * Returns error if not cached.
188
+ */
189
+ async resolve(uri: string, type: ModelType): Promise<LlmResult<string>> {
190
+ const parsed = parseModelUri(uri);
191
+ if (!parsed.ok) {
192
+ return { ok: false, error: invalidUriError(uri, parsed.error) };
193
+ }
194
+
195
+ // Local files: verify existence
196
+ if (parsed.value.scheme === 'file') {
197
+ const exists = await this.fileExists(parsed.value.file);
198
+ if (!exists) {
199
+ return {
200
+ ok: false,
201
+ error: modelNotFoundError(
202
+ uri,
203
+ `File not found: ${parsed.value.file}`
204
+ ),
205
+ };
206
+ }
207
+ return { ok: true, value: parsed.value.file };
208
+ }
209
+
210
+ // HF models: check cache
211
+ const cached = await this.getCachedPath(uri);
212
+ if (cached) {
213
+ return { ok: true, value: cached };
214
+ }
215
+
216
+ return { ok: false, error: modelNotCachedError(uri, type) };
217
+ }
218
+
219
+ /**
220
+ * Download a model to the cache.
221
+ * Uses node-llama-cpp's resolveModelFile for HF models.
222
+ */
223
+ async download(
224
+ uri: string,
225
+ type: ModelType,
226
+ onProgress?: ProgressCallback,
227
+ force?: boolean
228
+ ): Promise<LlmResult<string>> {
229
+ const parsed = parseModelUri(uri);
230
+ if (!parsed.ok) {
231
+ return { ok: false, error: invalidUriError(uri, parsed.error) };
232
+ }
233
+
234
+ // Local files: just verify
235
+ if (parsed.value.scheme === 'file') {
236
+ const exists = await this.fileExists(parsed.value.file);
237
+ if (!exists) {
238
+ return {
239
+ ok: false,
240
+ error: modelNotFoundError(
241
+ uri,
242
+ `File not found: ${parsed.value.file}`
243
+ ),
244
+ };
245
+ }
246
+ return { ok: true, value: parsed.value.file };
247
+ }
248
+
249
+ // Ensure cache dir exists
250
+ await mkdir(this.dir, { recursive: true });
251
+
252
+ // Force: delete existing file to trigger re-download
253
+ if (force) {
254
+ const existingPath = await this.getCachedPath(uri);
255
+ if (existingPath) {
256
+ await rm(existingPath).catch(() => {
257
+ // Ignore: file may not exist or already deleted
258
+ });
259
+ }
260
+ }
261
+
262
+ try {
263
+ const { resolveModelFile } = await import('node-llama-cpp');
264
+
265
+ // Convert to node-llama-cpp format (handles quantization shorthand)
266
+ // node-llama-cpp needs hf: prefix to identify HuggingFace models
267
+ const hfUri = toNodeLlamaCppUri(parsed.value);
268
+
269
+ const resolvedPath = await resolveModelFile(hfUri, {
270
+ directory: this.dir,
271
+ onProgress: onProgress
272
+ ? (status: unknown) => {
273
+ // Type-safe check for download progress status
274
+ if (
275
+ status &&
276
+ typeof status === 'object' &&
277
+ 'type' in status &&
278
+ status.type === 'download' &&
279
+ 'downloadedSize' in status &&
280
+ 'totalSize' in status
281
+ ) {
282
+ const s = status as {
283
+ downloadedSize: number;
284
+ totalSize: number;
285
+ };
286
+ const progress: DownloadProgress = {
287
+ downloadedBytes: s.downloadedSize,
288
+ totalBytes: s.totalSize,
289
+ percent:
290
+ s.totalSize > 0
291
+ ? (s.downloadedSize / s.totalSize) * 100
292
+ : 0,
293
+ };
294
+ onProgress(progress);
295
+ }
296
+ }
297
+ : undefined,
298
+ });
299
+
300
+ // Update manifest
301
+ await this.addToManifest(uri, type, resolvedPath);
302
+
303
+ return { ok: true, value: resolvedPath };
304
+ } catch (e) {
305
+ return { ok: false, error: downloadFailedError(uri, e) };
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Check if a model is cached/available.
311
+ * For file: URIs, checks if file exists on disk.
312
+ * For hf: URIs, checks the manifest.
313
+ */
314
+ async isCached(uri: string): Promise<boolean> {
315
+ const cached = await this.getCachedPath(uri);
316
+ return cached !== null;
317
+ }
318
+
319
+ /**
320
+ * Get cached/available path for a URI.
321
+ * For file: URIs, returns path if file exists.
322
+ * For hf: URIs, checks the manifest.
323
+ */
324
+ async getCachedPath(uri: string): Promise<string | null> {
325
+ // Handle file: URIs directly (check filesystem, not manifest)
326
+ const parsed = parseModelUri(uri);
327
+ if (parsed.ok && parsed.value.scheme === 'file') {
328
+ const exists = await this.fileExists(parsed.value.file);
329
+ return exists ? parsed.value.file : null;
330
+ }
331
+
332
+ // HF URIs: check manifest
333
+ const manifest = await this.loadManifest();
334
+ const entry = manifest.models.find((m) => m.uri === uri);
335
+ if (!entry) {
336
+ return null;
337
+ }
338
+
339
+ // Verify file still exists
340
+ const exists = await this.fileExists(entry.path);
341
+ if (!exists) {
342
+ // Remove stale entry
343
+ await this.removeFromManifest(uri);
344
+ return null;
345
+ }
346
+
347
+ return entry.path;
348
+ }
349
+
350
+ /**
351
+ * List all cached models.
352
+ */
353
+ async list(): Promise<ModelCacheEntry[]> {
354
+ const manifest = await this.loadManifest();
355
+ return manifest.models;
356
+ }
357
+
358
+ /**
359
+ * Get total size of all cached models.
360
+ */
361
+ async totalSize(): Promise<number> {
362
+ const manifest = await this.loadManifest();
363
+ return manifest.models.reduce((sum, m) => sum + (m.size || 0), 0);
364
+ }
365
+
366
+ /**
367
+ * Clear cached models.
368
+ * If types provided, only clears models of those types.
369
+ */
370
+ async clear(types?: ModelType[]): Promise<void> {
371
+ const manifest = await this.loadManifest();
372
+
373
+ const toRemove = types
374
+ ? manifest.models.filter((m) => types.includes(m.type))
375
+ : manifest.models;
376
+
377
+ for (const entry of toRemove) {
378
+ try {
379
+ await rm(entry.path, { force: true });
380
+ } catch {
381
+ // Ignore deletion errors
382
+ }
383
+ }
384
+
385
+ // Update manifest
386
+ if (types) {
387
+ manifest.models = manifest.models.filter((m) => !types.includes(m.type));
388
+ } else {
389
+ manifest.models = [];
390
+ }
391
+
392
+ await this.saveManifest(manifest);
393
+ }
394
+
395
+ // ───────────────────────────────────────────────────────────────────────────
396
+ // Private
397
+ // ───────────────────────────────────────────────────────────────────────────
398
+
399
+ private async fileExists(path: string): Promise<boolean> {
400
+ try {
401
+ await stat(path);
402
+ return true;
403
+ } catch {
404
+ return false;
405
+ }
406
+ }
407
+
408
+ private async loadManifest(): Promise<Manifest> {
409
+ if (this.manifest) {
410
+ return this.manifest;
411
+ }
412
+
413
+ try {
414
+ const content = await readFile(this.manifestPath, 'utf-8');
415
+ this.manifest = JSON.parse(content) as Manifest;
416
+ return this.manifest;
417
+ } catch {
418
+ // No manifest or invalid - create empty
419
+ this.manifest = { version: MANIFEST_VERSION, models: [] };
420
+ return this.manifest;
421
+ }
422
+ }
423
+
424
+ private async saveManifest(manifest: Manifest): Promise<void> {
425
+ await mkdir(this.dir, { recursive: true });
426
+ await writeFile(this.manifestPath, JSON.stringify(manifest, null, 2));
427
+ this.manifest = manifest;
428
+ }
429
+
430
+ private async addToManifest(
431
+ uri: string,
432
+ type: ModelType,
433
+ path: string
434
+ ): Promise<void> {
435
+ const manifest = await this.loadManifest();
436
+
437
+ // Get file size and compute checksum
438
+ let size = 0;
439
+ try {
440
+ const stats = await stat(path);
441
+ size = stats.size;
442
+ } catch {
443
+ // Ignore
444
+ }
445
+
446
+ // Remove existing entry if present
447
+ manifest.models = manifest.models.filter((m) => m.uri !== uri);
448
+
449
+ // Add new entry
450
+ manifest.models.push({
451
+ uri,
452
+ type,
453
+ path,
454
+ size,
455
+ checksum: '', // TODO: compute SHA-256 for large files
456
+ cachedAt: new Date().toISOString(),
457
+ });
458
+
459
+ await this.saveManifest(manifest);
460
+ }
461
+
462
+ private async removeFromManifest(uri: string): Promise<void> {
463
+ const manifest = await this.loadManifest();
464
+ manifest.models = manifest.models.filter((m) => m.uri !== uri);
465
+ await this.saveManifest(manifest);
466
+ }
467
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * LLM error types and helpers.
3
+ * Follows the pattern from converters/errors.ts
4
+ *
5
+ * @module src/llm/errors
6
+ */
7
+
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ // Error Types
10
+ // ─────────────────────────────────────────────────────────────────────────────
11
+
12
+ export type LlmErrorCode =
13
+ | 'MODEL_NOT_FOUND'
14
+ | 'MODEL_NOT_CACHED'
15
+ | 'MODEL_DOWNLOAD_FAILED'
16
+ | 'MODEL_LOAD_FAILED'
17
+ | 'MODEL_CORRUPTED'
18
+ | 'INFERENCE_FAILED'
19
+ | 'TIMEOUT'
20
+ | 'OUT_OF_MEMORY'
21
+ | 'INVALID_URI';
22
+
23
+ export interface LlmError {
24
+ code: LlmErrorCode;
25
+ message: string;
26
+ modelUri?: string;
27
+ retryable: boolean;
28
+ cause?: unknown;
29
+ suggestion?: string;
30
+ }
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Constants
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+
36
+ const MAX_CAUSE_LENGTH = 1000;
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Helpers
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Normalize a cause to safe, serializable format.
44
+ */
45
+ function normalizeCause(
46
+ cause: unknown
47
+ ): { name: string; message: string } | string | undefined {
48
+ if (cause === undefined || cause === null) {
49
+ return;
50
+ }
51
+
52
+ if (cause instanceof Error) {
53
+ const message =
54
+ cause.message.length > MAX_CAUSE_LENGTH
55
+ ? `${cause.message.slice(0, MAX_CAUSE_LENGTH)}...`
56
+ : cause.message;
57
+ return { name: cause.name, message };
58
+ }
59
+
60
+ if (typeof cause === 'string') {
61
+ return cause.length > MAX_CAUSE_LENGTH
62
+ ? `${cause.slice(0, MAX_CAUSE_LENGTH)}...`
63
+ : cause;
64
+ }
65
+
66
+ try {
67
+ const str = String(cause);
68
+ return str.length > MAX_CAUSE_LENGTH
69
+ ? `${str.slice(0, MAX_CAUSE_LENGTH)}...`
70
+ : str;
71
+ } catch {
72
+ return '[unserializable cause]';
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Create an LlmError with normalized cause.
78
+ */
79
+ export function llmError(
80
+ code: LlmErrorCode,
81
+ opts: Omit<LlmError, 'code'>
82
+ ): LlmError {
83
+ return {
84
+ code,
85
+ ...opts,
86
+ cause: normalizeCause(opts.cause),
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Check if error is retryable.
92
+ */
93
+ export function isRetryable(code: LlmErrorCode): boolean {
94
+ return ['MODEL_DOWNLOAD_FAILED', 'TIMEOUT', 'INFERENCE_FAILED'].includes(
95
+ code
96
+ );
97
+ }
98
+
99
+ // ─────────────────────────────────────────────────────────────────────────────
100
+ // Error Factories
101
+ // ─────────────────────────────────────────────────────────────────────────────
102
+
103
+ export function modelNotFoundError(uri: string, details?: string): LlmError {
104
+ return llmError('MODEL_NOT_FOUND', {
105
+ message: details
106
+ ? `Model not found: ${details}`
107
+ : `Model not found: ${uri}`,
108
+ modelUri: uri,
109
+ retryable: false,
110
+ });
111
+ }
112
+
113
+ export function modelNotCachedError(
114
+ uri: string,
115
+ modelType: 'embed' | 'rerank' | 'gen'
116
+ ): LlmError {
117
+ return llmError('MODEL_NOT_CACHED', {
118
+ message: `${modelType} model not cached`,
119
+ modelUri: uri,
120
+ retryable: false,
121
+ suggestion: `Run: gno models pull --${modelType}`,
122
+ });
123
+ }
124
+
125
+ export function downloadFailedError(uri: string, cause?: unknown): LlmError {
126
+ return llmError('MODEL_DOWNLOAD_FAILED', {
127
+ message: `Failed to download model: ${uri}`,
128
+ modelUri: uri,
129
+ retryable: true,
130
+ cause,
131
+ });
132
+ }
133
+
134
+ export function loadFailedError(uri: string, cause?: unknown): LlmError {
135
+ return llmError('MODEL_LOAD_FAILED', {
136
+ message: `Failed to load model: ${uri}`,
137
+ modelUri: uri,
138
+ retryable: false,
139
+ cause,
140
+ suggestion: 'Run: gno doctor',
141
+ });
142
+ }
143
+
144
+ export function corruptedError(uri: string, cause?: unknown): LlmError {
145
+ return llmError('MODEL_CORRUPTED', {
146
+ message: `Model file corrupted: ${uri}`,
147
+ modelUri: uri,
148
+ retryable: false,
149
+ cause,
150
+ suggestion: 'Run: gno models clear && gno models pull',
151
+ });
152
+ }
153
+
154
+ export function inferenceFailedError(uri: string, cause?: unknown): LlmError {
155
+ return llmError('INFERENCE_FAILED', {
156
+ message: `Inference failed for model: ${uri}`,
157
+ modelUri: uri,
158
+ retryable: true,
159
+ cause,
160
+ });
161
+ }
162
+
163
+ export function timeoutError(
164
+ uri: string,
165
+ operation: 'load' | 'inference',
166
+ timeoutMs: number
167
+ ): LlmError {
168
+ return llmError('TIMEOUT', {
169
+ message: `${operation} timed out after ${timeoutMs}ms`,
170
+ modelUri: uri,
171
+ retryable: true,
172
+ });
173
+ }
174
+
175
+ export function outOfMemoryError(uri: string, cause?: unknown): LlmError {
176
+ return llmError('OUT_OF_MEMORY', {
177
+ message: `Out of memory loading model: ${uri}`,
178
+ modelUri: uri,
179
+ retryable: false,
180
+ cause,
181
+ suggestion: 'Try a smaller quantization (Q4_K_M) or close other apps',
182
+ });
183
+ }
184
+
185
+ export function invalidUriError(uri: string, details: string): LlmError {
186
+ return llmError('INVALID_URI', {
187
+ message: `Invalid model URI: ${details}`,
188
+ modelUri: uri,
189
+ retryable: false,
190
+ });
191
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * LLM subsystem public API.
3
+ *
4
+ * @module src/llm
5
+ */
6
+
7
+ // Re-export config types (source of truth in config/types.ts)
8
+ export type { ModelConfig, ModelPreset } from '../config/types';
9
+ export type { ParsedModelUri } from './cache';
10
+ // Cache
11
+ export { ModelCache, parseModelUri, toNodeLlamaCppUri } from './cache';
12
+ // Errors
13
+ export type { LlmError, LlmErrorCode } from './errors';
14
+ export {
15
+ corruptedError,
16
+ downloadFailedError,
17
+ inferenceFailedError,
18
+ invalidUriError,
19
+ isRetryable,
20
+ llmError,
21
+ loadFailedError,
22
+ modelNotCachedError,
23
+ modelNotFoundError,
24
+ outOfMemoryError,
25
+ timeoutError,
26
+ } from './errors';
27
+ // Adapter
28
+ export { createLlmAdapter, LlmAdapter } from './nodeLlamaCpp/adapter';
29
+ // Lifecycle
30
+ export {
31
+ getModelManager,
32
+ ModelManager,
33
+ resetModelManager,
34
+ } from './nodeLlamaCpp/lifecycle';
35
+ // Registry
36
+ export {
37
+ getActivePreset,
38
+ getModelConfig,
39
+ getPreset,
40
+ listPresets,
41
+ resolveModelUri,
42
+ } from './registry';
43
+ // Types
44
+ export type {
45
+ DownloadProgress,
46
+ EmbeddingPort,
47
+ GenerationPort,
48
+ GenParams,
49
+ LlmResult,
50
+ LoadedModel,
51
+ ModelCacheEntry,
52
+ ModelStatus,
53
+ ModelType,
54
+ ModelUri,
55
+ ProgressCallback,
56
+ RerankPort,
57
+ RerankScore,
58
+ } from './types';