@voidwire/lore 0.1.8 → 0.1.9
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/cli.ts +0 -5
- package/index.ts +0 -1
- package/lib/semantic.ts +45 -93
- package/package.json +4 -1
package/cli.ts
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
captureNote,
|
|
32
32
|
captureTeaching,
|
|
33
33
|
semanticSearch,
|
|
34
|
-
isOllamaAvailable,
|
|
35
34
|
hasEmbeddings,
|
|
36
35
|
DOMAINS,
|
|
37
36
|
type SearchResult,
|
|
@@ -252,10 +251,6 @@ async function handleSearch(args: string[]): Promise<void> {
|
|
|
252
251
|
fail("No embeddings found. Run lore-embed-all first.", 2);
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
if (!(await isOllamaAvailable())) {
|
|
256
|
-
fail("Ollama not available. Start Ollama or check SQLITE_VEC_PATH.", 2);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
254
|
try {
|
|
260
255
|
const results = await semanticSearch(query, { source, limit });
|
|
261
256
|
output({
|
package/index.ts
CHANGED
package/lib/semantic.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* lib/semantic.ts - Semantic search via
|
|
2
|
+
* lib/semantic.ts - Semantic search via local embeddings
|
|
3
3
|
*
|
|
4
|
-
* Query embedding
|
|
4
|
+
* Query embedding using @huggingface/transformers with nomic-embed-text-v1.5.
|
|
5
|
+
* KNN search against sqlite-vec virtual table.
|
|
5
6
|
* Uses Bun's built-in SQLite with sqlite-vec extension.
|
|
6
7
|
*
|
|
7
8
|
* Note: macOS ships Apple's SQLite which disables extension loading.
|
|
@@ -10,7 +11,8 @@
|
|
|
10
11
|
|
|
11
12
|
import { Database } from "bun:sqlite";
|
|
12
13
|
import { homedir } from "os";
|
|
13
|
-
import { existsSync
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { pipeline } from "@huggingface/transformers";
|
|
14
16
|
|
|
15
17
|
// Use Homebrew SQLite on macOS to enable extension loading
|
|
16
18
|
// Must be called before any Database instances are created
|
|
@@ -32,120 +34,70 @@ export interface SemanticSearchOptions {
|
|
|
32
34
|
limit?: number;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const MODEL_NAME = "nomic-ai/nomic-embed-text-v1.5";
|
|
38
|
+
|
|
39
|
+
interface EmbeddingPipeline {
|
|
40
|
+
(
|
|
41
|
+
text: string,
|
|
42
|
+
options?: { pooling?: string; normalize?: boolean },
|
|
43
|
+
): Promise<{
|
|
44
|
+
data: Float32Array;
|
|
45
|
+
}>;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
model: "nomic-embed-text",
|
|
43
|
-
};
|
|
48
|
+
// Cache the pipeline to avoid reloading on every query
|
|
49
|
+
let cachedPipeline: EmbeddingPipeline | null = null;
|
|
44
50
|
|
|
45
51
|
function getDatabasePath(): string {
|
|
46
52
|
return `${homedir()}/.local/share/lore/lore.db`;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
function getConfigPath(): string {
|
|
50
|
-
return `${homedir()}/.config/lore/config.toml`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
+
* Get or create the embedding pipeline
|
|
57
|
+
* Pipeline is cached after first load for performance
|
|
56
58
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!existsSync(configPath)) {
|
|
61
|
-
return DEFAULT_CONFIG;
|
|
59
|
+
async function getEmbeddingPipeline(): Promise<EmbeddingPipeline> {
|
|
60
|
+
if (cachedPipeline) {
|
|
61
|
+
return cachedPipeline;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
// Extract [embedding].endpoint first
|
|
68
|
-
const endpointMatch = content.match(
|
|
69
|
-
/\[embedding\][^[]*endpoint\s*=\s*"([^"]+)"/s,
|
|
70
|
-
);
|
|
71
|
-
if (endpointMatch) {
|
|
72
|
-
const modelMatch = content.match(
|
|
73
|
-
/\[embedding\][^[]*model\s*=\s*"([^"]+)"/s,
|
|
74
|
-
);
|
|
75
|
-
return {
|
|
76
|
-
endpoint: endpointMatch[1],
|
|
77
|
-
model: modelMatch?.[1] ?? DEFAULT_CONFIG.model,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Fall back to [llm].api_base
|
|
82
|
-
const apiBaseMatch = content.match(/\[llm\][^[]*api_base\s*=\s*"([^"]+)"/s);
|
|
83
|
-
if (apiBaseMatch) {
|
|
84
|
-
const modelMatch = content.match(
|
|
85
|
-
/\[embedding\][^[]*model\s*=\s*"([^"]+)"/s,
|
|
86
|
-
);
|
|
87
|
-
return {
|
|
88
|
-
endpoint: apiBaseMatch[1],
|
|
89
|
-
model: modelMatch?.[1] ?? DEFAULT_CONFIG.model,
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return DEFAULT_CONFIG;
|
|
94
|
-
} catch {
|
|
95
|
-
return DEFAULT_CONFIG;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Check if Ollama is available at configured endpoint
|
|
101
|
-
*/
|
|
102
|
-
export async function isOllamaAvailable(): Promise<boolean> {
|
|
103
|
-
const config = loadEmbeddingConfig();
|
|
104
|
-
try {
|
|
105
|
-
const controller = new AbortController();
|
|
106
|
-
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
107
|
-
|
|
108
|
-
const response = await fetch(`${config.endpoint}/api/tags`, {
|
|
109
|
-
method: "GET",
|
|
110
|
-
signal: controller.signal,
|
|
65
|
+
const p = await pipeline("feature-extraction", MODEL_NAME, {
|
|
66
|
+
dtype: "fp32",
|
|
111
67
|
});
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
68
|
+
cachedPipeline = p as unknown as EmbeddingPipeline;
|
|
69
|
+
return cachedPipeline;
|
|
70
|
+
} catch (error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to load embedding model: ${message}\n` +
|
|
74
|
+
`Note: First run downloads ~500MB model to ~/.cache/huggingface/hub`,
|
|
75
|
+
);
|
|
117
76
|
}
|
|
118
77
|
}
|
|
119
78
|
|
|
120
79
|
/**
|
|
121
|
-
* Embed a query string using
|
|
80
|
+
* Embed a query string using local transformers.js model
|
|
81
|
+
* Uses "search_query: " prefix as required by nomic-embed-text
|
|
122
82
|
* @returns 768-dimensional embedding vector
|
|
123
83
|
*/
|
|
124
84
|
export async function embedQuery(query: string): Promise<number[]> {
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
prompt: query,
|
|
134
|
-
}),
|
|
85
|
+
const embedder = await getEmbeddingPipeline();
|
|
86
|
+
|
|
87
|
+
// nomic model requires "search_query: " prefix for queries
|
|
88
|
+
// (FastEmbed uses "search_document: " prefix during indexing)
|
|
89
|
+
const prefixedQuery = `search_query: ${query}`;
|
|
90
|
+
const output = await embedder(prefixedQuery, {
|
|
91
|
+
pooling: "mean",
|
|
92
|
+
normalize: true,
|
|
135
93
|
});
|
|
136
94
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
`Ollama API error: ${response.status} ${response.statusText}`,
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const result = (await response.json()) as { embedding?: number[] };
|
|
144
|
-
const embedding = result.embedding;
|
|
95
|
+
// Output is a Tensor, convert to array
|
|
96
|
+
const embedding = Array.from(output.data as Float32Array);
|
|
145
97
|
|
|
146
|
-
if (
|
|
98
|
+
if (embedding.length !== 768) {
|
|
147
99
|
throw new Error(
|
|
148
|
-
`Invalid embedding: expected 768 dims, got ${embedding
|
|
100
|
+
`Invalid embedding: expected 768 dims, got ${embedding.length}`,
|
|
149
101
|
);
|
|
150
102
|
}
|
|
151
103
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidwire/lore",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"engines": {
|
|
42
42
|
"bun": ">=1.0.0"
|
|
43
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@huggingface/transformers": "^3.2.6"
|
|
46
|
+
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"bun-types": "1.3.5"
|
|
46
49
|
},
|