@voidwire/lore 1.0.7 → 1.1.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/lib/config.ts +27 -1
- package/lib/indexers/personal.ts +93 -5
- package/lib/semantic.ts +2 -2
- package/package.json +3 -2
package/lib/config.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface LoreConfig {
|
|
|
33
33
|
sqlite: string;
|
|
34
34
|
custom_sqlite?: string;
|
|
35
35
|
};
|
|
36
|
+
embedding: {
|
|
37
|
+
model: string;
|
|
38
|
+
dimensions: number;
|
|
39
|
+
};
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
let cachedConfig: LoreConfig | null = null;
|
|
@@ -81,9 +85,27 @@ export function getConfig(): LoreConfig {
|
|
|
81
85
|
"Invalid config: missing [database] section in config.toml",
|
|
82
86
|
);
|
|
83
87
|
}
|
|
88
|
+
if (!parsed.embedding || typeof parsed.embedding !== "object") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
"Invalid config: missing [embedding] section in config.toml",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
84
93
|
|
|
85
94
|
const paths = parsed.paths as Record<string, unknown>;
|
|
86
95
|
const database = parsed.database as Record<string, unknown>;
|
|
96
|
+
const embedding = parsed.embedding as Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
// Validate required embedding fields
|
|
99
|
+
if (typeof embedding.model !== "string") {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"Invalid config: embedding.model is missing or not a string",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
if (typeof embedding.dimensions !== "number") {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"Invalid config: embedding.dimensions is missing or not a number",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
87
109
|
|
|
88
110
|
// Validate required path fields
|
|
89
111
|
const requiredPaths = [
|
|
@@ -140,7 +162,11 @@ export function getConfig(): LoreConfig {
|
|
|
140
162
|
? resolvePath(database.custom_sqlite)
|
|
141
163
|
: undefined,
|
|
142
164
|
},
|
|
165
|
+
embedding: {
|
|
166
|
+
model: embedding.model as string,
|
|
167
|
+
dimensions: embedding.dimensions as number,
|
|
168
|
+
},
|
|
143
169
|
};
|
|
144
170
|
|
|
145
|
-
return cachedConfig
|
|
171
|
+
return cachedConfig!;
|
|
146
172
|
}
|
package/lib/indexers/personal.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { readFileSync, statSync, existsSync } from "fs";
|
|
14
14
|
import { join } from "path";
|
|
15
15
|
import { checkPath, type IndexerContext } from "../indexer";
|
|
16
|
+
import { complete } from "@voidwire/llm-core";
|
|
16
17
|
|
|
17
18
|
function fileMtime(path: string): string {
|
|
18
19
|
return statSync(path).mtime.toISOString();
|
|
@@ -24,7 +25,65 @@ function toISO(dateStr: string, fallback: string): string {
|
|
|
24
25
|
return s.includes("T") ? s : `${s.slice(0, 10)}T00:00:00Z`;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
const ENRICH_PROMPTS: Record<string, string> = {
|
|
29
|
+
person: `You are enriching a personal contact entry for search indexing.
|
|
30
|
+
The "relationship" field is the EXACT relationship — do NOT add other relationship types.
|
|
31
|
+
Generate synonyms and alternative phrasings ONLY for the stated relationship.
|
|
32
|
+
Example: relationship "uncle" → uncle, family member, relative, parent's brother, parent's sibling. NOT: cousin, nephew, aunt.
|
|
33
|
+
Example: relationship "daughter" → daughter, child, kid, offspring, family member. NOT: son, niece, nephew.
|
|
34
|
+
Include both singular and plural forms where applicable.
|
|
35
|
+
Keep under 80 words. Output only the description, no headers or formatting.`,
|
|
36
|
+
book: `You are enriching a book entry for search indexing.
|
|
37
|
+
Generate: genre, themes, subject matter, and related topics.
|
|
38
|
+
Include both singular and plural forms of key terms.
|
|
39
|
+
Keep under 80 words. Output only the description, no headers or formatting.`,
|
|
40
|
+
movie: `You are enriching a movie entry for search indexing.
|
|
41
|
+
Generate: genre, themes, notable actors or director if well-known, and related topics.
|
|
42
|
+
Include both singular and plural forms of key terms.
|
|
43
|
+
Keep under 80 words. Output only the description, no headers or formatting.`,
|
|
44
|
+
interest: `You are enriching a personal interest entry for search indexing.
|
|
45
|
+
Generate: related activities, domains, synonyms, and common alternative phrasings.
|
|
46
|
+
Include both singular and plural forms.
|
|
47
|
+
Keep under 80 words. Output only the description, no headers or formatting.`,
|
|
48
|
+
habit: `You are enriching a personal habit/routine entry for search indexing.
|
|
49
|
+
Generate: related routines, synonyms, categories, and common alternative phrasings.
|
|
50
|
+
Include both singular and plural forms.
|
|
51
|
+
Keep under 80 words. Output only the description, no headers or formatting.`,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const ENRICH_TIMEOUT_MS = 10_000;
|
|
55
|
+
|
|
56
|
+
let enrichmentDisabled = false;
|
|
57
|
+
|
|
58
|
+
async function enrich(type: string, input: string): Promise<string | null> {
|
|
59
|
+
if (enrichmentDisabled) return null;
|
|
60
|
+
const systemPrompt = ENRICH_PROMPTS[type];
|
|
61
|
+
if (!systemPrompt) return null;
|
|
62
|
+
try {
|
|
63
|
+
const result = await Promise.race([
|
|
64
|
+
complete({
|
|
65
|
+
prompt: input,
|
|
66
|
+
systemPrompt,
|
|
67
|
+
temperature: 0.3,
|
|
68
|
+
maxTokens: 150,
|
|
69
|
+
}),
|
|
70
|
+
new Promise<never>((_, reject) =>
|
|
71
|
+
setTimeout(
|
|
72
|
+
() => reject(new Error("Enrichment timed out")),
|
|
73
|
+
ENRICH_TIMEOUT_MS,
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
]);
|
|
77
|
+
return result.text.replace(/<\|[^|]+\|>/g, "").trim();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.warn(`Enrichment failed, disabling for remaining entries: ${e}`);
|
|
80
|
+
enrichmentDisabled = true;
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
27
85
|
export async function indexPersonal(ctx: IndexerContext): Promise<void> {
|
|
86
|
+
enrichmentDisabled = false;
|
|
28
87
|
const personalDir = ctx.config.paths.personal;
|
|
29
88
|
|
|
30
89
|
if (!checkPath("personal", "paths.personal", personalDir)) return;
|
|
@@ -37,7 +96,12 @@ export async function indexPersonal(ctx: IndexerContext): Promise<void> {
|
|
|
37
96
|
const books = JSON.parse(readFileSync(booksPath, "utf-8"));
|
|
38
97
|
for (const book of books) {
|
|
39
98
|
if (!book.title) continue;
|
|
40
|
-
|
|
99
|
+
let content = `${book.title} by ${book.author || "unknown"}\n${book.notes || ""}`;
|
|
100
|
+
const enriched = await enrich(
|
|
101
|
+
"book",
|
|
102
|
+
JSON.stringify({ title: book.title, author: book.author }),
|
|
103
|
+
);
|
|
104
|
+
if (enriched) content = enriched;
|
|
41
105
|
const timestamp = book.date_read
|
|
42
106
|
? toISO(book.date_read, booksTs)
|
|
43
107
|
: booksTs;
|
|
@@ -65,7 +129,15 @@ export async function indexPersonal(ctx: IndexerContext): Promise<void> {
|
|
|
65
129
|
const people = JSON.parse(readFileSync(peoplePath, "utf-8"));
|
|
66
130
|
for (const person of people) {
|
|
67
131
|
if (!person.name) continue;
|
|
68
|
-
|
|
132
|
+
let content = `${person.name}\n${person.relationship || ""}\n${person.notes || ""}`;
|
|
133
|
+
const enriched = await enrich(
|
|
134
|
+
"person",
|
|
135
|
+
JSON.stringify({
|
|
136
|
+
name: person.name,
|
|
137
|
+
relationship: person.relationship,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
if (enriched) content = enriched;
|
|
69
141
|
|
|
70
142
|
ctx.insert({
|
|
71
143
|
source: "personal",
|
|
@@ -91,9 +163,14 @@ export async function indexPersonal(ctx: IndexerContext): Promise<void> {
|
|
|
91
163
|
for (const movie of movies) {
|
|
92
164
|
if (!movie.title) continue;
|
|
93
165
|
const year = movie.year || "";
|
|
94
|
-
|
|
166
|
+
let content = year
|
|
95
167
|
? `${movie.title} (${year})\n${movie.notes || ""}`
|
|
96
168
|
: `${movie.title}\n${movie.notes || ""}`;
|
|
169
|
+
const enriched = await enrich(
|
|
170
|
+
"movie",
|
|
171
|
+
JSON.stringify({ title: movie.title, year: movie.year }),
|
|
172
|
+
);
|
|
173
|
+
if (enriched) content = enriched;
|
|
97
174
|
const timestamp = movie.date_watched
|
|
98
175
|
? toISO(movie.date_watched, moviesTs)
|
|
99
176
|
: moviesTs;
|
|
@@ -147,11 +224,17 @@ export async function indexPersonal(ctx: IndexerContext): Promise<void> {
|
|
|
147
224
|
const interests = JSON.parse(readFileSync(interestsPath, "utf-8"));
|
|
148
225
|
for (const interest of interests) {
|
|
149
226
|
if (typeof interest !== "string" || !interest) continue;
|
|
227
|
+
let content = interest;
|
|
228
|
+
const enriched = await enrich(
|
|
229
|
+
"interest",
|
|
230
|
+
JSON.stringify({ name: interest }),
|
|
231
|
+
);
|
|
232
|
+
if (enriched) content = enriched;
|
|
150
233
|
|
|
151
234
|
ctx.insert({
|
|
152
235
|
source: "personal",
|
|
153
236
|
title: `[interest] ${interest}`,
|
|
154
|
-
content
|
|
237
|
+
content,
|
|
155
238
|
topic: "",
|
|
156
239
|
type: "interest",
|
|
157
240
|
timestamp: interestsTs,
|
|
@@ -173,7 +256,12 @@ export async function indexPersonal(ctx: IndexerContext): Promise<void> {
|
|
|
173
256
|
const habitName = habit.habit || "";
|
|
174
257
|
if (!habitName) continue;
|
|
175
258
|
const frequency = habit.frequency || "";
|
|
176
|
-
|
|
259
|
+
let content = frequency ? `${habitName} (${frequency})` : habitName;
|
|
260
|
+
const enriched = await enrich(
|
|
261
|
+
"habit",
|
|
262
|
+
JSON.stringify({ habit: habitName, frequency }),
|
|
263
|
+
);
|
|
264
|
+
if (enriched) content = enriched;
|
|
177
265
|
|
|
178
266
|
ctx.insert({
|
|
179
267
|
source: "personal",
|
package/lib/semantic.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync } from "fs";
|
|
|
11
11
|
import { pipeline } from "@huggingface/transformers";
|
|
12
12
|
import { getDatabasePath, openDatabase } from "./db.js";
|
|
13
13
|
import { search as keywordSearch, type SearchResult } from "./search.js";
|
|
14
|
+
import { getConfig } from "./config.js";
|
|
14
15
|
|
|
15
16
|
export interface SemanticResult {
|
|
16
17
|
rowid: number;
|
|
@@ -30,8 +31,7 @@ export interface SemanticSearchOptions {
|
|
|
30
31
|
since?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const MODEL_NAME =
|
|
34
|
-
const EMBEDDING_DIM = 768;
|
|
34
|
+
const { model: MODEL_NAME, dimensions: EMBEDDING_DIM } = getConfig().embedding;
|
|
35
35
|
|
|
36
36
|
interface EmbeddingPipeline {
|
|
37
37
|
(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidwire/lore",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@huggingface/transformers": "^3.2.6",
|
|
47
|
-
"@iarna/toml": "^2.2.5"
|
|
47
|
+
"@iarna/toml": "^2.2.5",
|
|
48
|
+
"@voidwire/llm-core": "^0.3.1"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"bun-types": "1.3.5"
|