@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 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
  }
@@ -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
- const content = `${book.title} by ${book.author || "unknown"}\n${book.notes || ""}`;
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
- const content = `${person.name}\n${person.relationship || ""}\n${person.notes || ""}`;
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
- const content = year
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: interest,
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
- const content = frequency ? `${habitName} (${frequency})` : habitName;
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 = "nomic-ai/nomic-embed-text-v1.5";
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.7",
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"