@voidwire/lore 0.9.0 → 1.0.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/cli.ts +66 -1
- package/lib/config.ts +134 -0
- package/lib/indexer.ts +213 -0
- package/lib/indexers/blogs.ts +146 -0
- package/lib/indexers/captures.ts +105 -0
- package/lib/indexers/commits.ts +90 -0
- package/lib/indexers/development.ts +68 -0
- package/lib/indexers/events.ts +61 -0
- package/lib/indexers/explorations.ts +89 -0
- package/lib/indexers/flux.ts +142 -0
- package/lib/indexers/index.ts +41 -0
- package/lib/indexers/insights.ts +53 -0
- package/lib/indexers/learnings.ts +53 -0
- package/lib/indexers/observations.ts +53 -0
- package/lib/indexers/obsidian.ts +151 -0
- package/lib/indexers/personal.ts +262 -0
- package/lib/indexers/readmes.ts +49 -0
- package/lib/indexers/sessions.ts +127 -0
- package/lib/indexers/teachings.ts +52 -0
- package/lib/info.ts +3 -3
- package/lib/list.ts +23 -32
- package/lib/projects.ts +26 -31
- package/lib/realtime.ts +51 -19
- package/lib/search.ts +5 -7
- package/lib/semantic.ts +6 -31
- package/package.json +9 -8
package/lib/list.ts
CHANGED
|
@@ -65,19 +65,6 @@ const PERSONAL_SUBTYPES: Partial<Record<Source, string>> = {
|
|
|
65
65
|
habits: "habit",
|
|
66
66
|
};
|
|
67
67
|
|
|
68
|
-
// Maps source to metadata field for --project filter
|
|
69
|
-
// Project-based domains use "project", topic-based domains use "topic"
|
|
70
|
-
const PROJECT_FIELD: Record<string, string> = {
|
|
71
|
-
commits: "project",
|
|
72
|
-
sessions: "project",
|
|
73
|
-
flux: "project",
|
|
74
|
-
insights: "topic",
|
|
75
|
-
captures: "topic",
|
|
76
|
-
teachings: "topic",
|
|
77
|
-
learnings: "topic",
|
|
78
|
-
observations: "topic",
|
|
79
|
-
};
|
|
80
|
-
|
|
81
68
|
export interface ListOptions {
|
|
82
69
|
limit?: number;
|
|
83
70
|
project?: string;
|
|
@@ -87,6 +74,8 @@ export interface ListOptions {
|
|
|
87
74
|
export interface ListEntry {
|
|
88
75
|
title: string;
|
|
89
76
|
content: string;
|
|
77
|
+
topic: string;
|
|
78
|
+
type: string;
|
|
90
79
|
metadata: Record<string, unknown>;
|
|
91
80
|
}
|
|
92
81
|
|
|
@@ -104,6 +93,8 @@ function getDatabasePath(): string {
|
|
|
104
93
|
interface RawRow {
|
|
105
94
|
title: string;
|
|
106
95
|
content: string;
|
|
96
|
+
topic: string;
|
|
97
|
+
type: string;
|
|
107
98
|
metadata: string;
|
|
108
99
|
}
|
|
109
100
|
|
|
@@ -117,26 +108,24 @@ function queryBySource(
|
|
|
117
108
|
project?: string,
|
|
118
109
|
type?: string,
|
|
119
110
|
): ListEntry[] {
|
|
120
|
-
let sql =
|
|
111
|
+
let sql =
|
|
112
|
+
"SELECT title, content, topic, type, metadata FROM search WHERE source = ?";
|
|
121
113
|
const params: (string | number)[] = [source];
|
|
122
114
|
|
|
123
|
-
// Add project filter if provided
|
|
115
|
+
// Add project filter if provided — uses topic column directly
|
|
124
116
|
if (project) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
sql += ` AND json_extract(metadata, '$.${field}') = ?`;
|
|
128
|
-
params.push(project);
|
|
129
|
-
}
|
|
117
|
+
sql += " AND topic = ?";
|
|
118
|
+
params.push(project);
|
|
130
119
|
}
|
|
131
120
|
|
|
132
|
-
// Add type filter if provided
|
|
133
|
-
if (type
|
|
134
|
-
sql +=
|
|
121
|
+
// Add type filter if provided — uses type column directly
|
|
122
|
+
if (type) {
|
|
123
|
+
sql += " AND type = ?";
|
|
135
124
|
params.push(type);
|
|
136
125
|
}
|
|
137
126
|
|
|
138
127
|
// Order by timestamp descending (most recent first)
|
|
139
|
-
sql += " ORDER BY
|
|
128
|
+
sql += " ORDER BY timestamp DESC";
|
|
140
129
|
|
|
141
130
|
if (limit) {
|
|
142
131
|
sql += " LIMIT ?";
|
|
@@ -149,6 +138,8 @@ function queryBySource(
|
|
|
149
138
|
return rows.map((row) => ({
|
|
150
139
|
title: row.title,
|
|
151
140
|
content: row.content,
|
|
141
|
+
topic: row.topic,
|
|
142
|
+
type: row.type,
|
|
152
143
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
153
144
|
}));
|
|
154
145
|
}
|
|
@@ -163,10 +154,10 @@ function queryPersonalType(
|
|
|
163
154
|
): ListEntry[] {
|
|
164
155
|
// Filter by type in SQL, not JS - avoids LIMIT truncation bug
|
|
165
156
|
let sql = `
|
|
166
|
-
SELECT title, content, metadata FROM search
|
|
157
|
+
SELECT title, content, topic, type, metadata FROM search
|
|
167
158
|
WHERE source = 'personal'
|
|
168
|
-
AND
|
|
169
|
-
ORDER BY
|
|
159
|
+
AND type = ?
|
|
160
|
+
ORDER BY timestamp DESC
|
|
170
161
|
`;
|
|
171
162
|
const params: (string | number)[] = [type];
|
|
172
163
|
|
|
@@ -181,6 +172,8 @@ function queryPersonalType(
|
|
|
181
172
|
return rows.map((row) => ({
|
|
182
173
|
title: row.title,
|
|
183
174
|
content: row.content,
|
|
175
|
+
topic: row.topic,
|
|
176
|
+
type: row.type,
|
|
184
177
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
185
178
|
}));
|
|
186
179
|
}
|
|
@@ -243,12 +236,10 @@ export function listSources(): Source[] {
|
|
|
243
236
|
}
|
|
244
237
|
|
|
245
238
|
/**
|
|
246
|
-
* Extract project name from entry
|
|
239
|
+
* Extract project name from entry
|
|
247
240
|
*/
|
|
248
|
-
function extractProjectFromEntry(entry: ListEntry,
|
|
249
|
-
|
|
250
|
-
if (!field) return "unknown";
|
|
251
|
-
return (entry.metadata[field] as string) || "unknown";
|
|
241
|
+
function extractProjectFromEntry(entry: ListEntry, _source: string): string {
|
|
242
|
+
return entry.topic || "unknown";
|
|
252
243
|
}
|
|
253
244
|
|
|
254
245
|
/**
|
package/lib/projects.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/projects.ts - List all known projects across sources
|
|
3
3
|
*
|
|
4
|
-
* Queries distinct
|
|
5
|
-
*
|
|
4
|
+
* Queries distinct topic values from the topic column across
|
|
5
|
+
* project-scoped sources.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Database } from "bun:sqlite";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import { existsSync } from "fs";
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
12
|
+
const PROJECT_SOURCES = [
|
|
13
|
+
"commits",
|
|
14
|
+
"sessions",
|
|
15
|
+
"flux",
|
|
16
|
+
"insights",
|
|
17
|
+
"captures",
|
|
18
|
+
"teachings",
|
|
19
|
+
"learnings",
|
|
20
|
+
"observations",
|
|
21
|
+
];
|
|
23
22
|
|
|
24
23
|
function getDatabasePath(): string {
|
|
25
24
|
return `${homedir()}/.local/share/lore/lore.db`;
|
|
@@ -40,24 +39,20 @@ export function projects(): string[] {
|
|
|
40
39
|
const db = new Database(dbPath, { readonly: true });
|
|
41
40
|
|
|
42
41
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return Array.from(allProjects).sort();
|
|
42
|
+
const placeholders = PROJECT_SOURCES.map(() => "?").join(", ");
|
|
43
|
+
const stmt = db.prepare(`
|
|
44
|
+
SELECT DISTINCT topic
|
|
45
|
+
FROM search
|
|
46
|
+
WHERE source IN (${placeholders})
|
|
47
|
+
AND topic IS NOT NULL
|
|
48
|
+
AND topic != ''
|
|
49
|
+
`);
|
|
50
|
+
const results = stmt.all(...PROJECT_SOURCES) as { topic: string }[];
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
.map((r) => r.topic)
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.sort();
|
|
61
56
|
} finally {
|
|
62
57
|
db.close();
|
|
63
58
|
}
|
package/lib/realtime.ts
CHANGED
|
@@ -87,13 +87,25 @@ function insertSearchEntry(db: Database, event: CaptureEvent): number {
|
|
|
87
87
|
const title = buildTitle(event);
|
|
88
88
|
const content = getContentForEmbedding(event);
|
|
89
89
|
const metadata = buildMetadata(event);
|
|
90
|
+
const data = event.data as Record<string, unknown>;
|
|
91
|
+
const topic = String(data.topic || "");
|
|
92
|
+
const type = extractType(event);
|
|
93
|
+
const timestamp = event.timestamp || new Date().toISOString();
|
|
90
94
|
|
|
91
95
|
const stmt = db.prepare(`
|
|
92
|
-
INSERT INTO search (source, title, content, metadata)
|
|
93
|
-
VALUES (?, ?, ?, ?)
|
|
96
|
+
INSERT INTO search (source, title, content, metadata, topic, type, timestamp)
|
|
97
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
94
98
|
`);
|
|
95
99
|
|
|
96
|
-
const result = stmt.run(
|
|
100
|
+
const result = stmt.run(
|
|
101
|
+
source,
|
|
102
|
+
title,
|
|
103
|
+
content,
|
|
104
|
+
metadata,
|
|
105
|
+
topic,
|
|
106
|
+
type,
|
|
107
|
+
timestamp,
|
|
108
|
+
);
|
|
97
109
|
return Number(result.lastInsertRowid);
|
|
98
110
|
}
|
|
99
111
|
|
|
@@ -147,10 +159,13 @@ function buildTitle(event: CaptureEvent): string {
|
|
|
147
159
|
|
|
148
160
|
/**
|
|
149
161
|
* Extract content for embedding from event
|
|
162
|
+
* Concatenates topic+content for richer embeddings (matches lore-embed-all)
|
|
150
163
|
*/
|
|
151
164
|
function getContentForEmbedding(event: CaptureEvent): string {
|
|
152
165
|
const data = event.data as Record<string, unknown>;
|
|
153
|
-
|
|
166
|
+
const content = String(data.content || data.text || "");
|
|
167
|
+
const topic = String(data.topic || "").trim();
|
|
168
|
+
return topic ? `${topic} ${content}`.trim() : content;
|
|
154
169
|
}
|
|
155
170
|
|
|
156
171
|
/**
|
|
@@ -158,19 +173,9 @@ function getContentForEmbedding(event: CaptureEvent): string {
|
|
|
158
173
|
*/
|
|
159
174
|
function buildMetadata(event: CaptureEvent): string {
|
|
160
175
|
const data = event.data as Record<string, unknown>;
|
|
161
|
-
const
|
|
162
|
-
const date = timestamp ? timestamp.substring(0, 10) : "";
|
|
163
|
-
|
|
164
|
-
const content = getContentForEmbedding(event);
|
|
165
|
-
const metadata: Record<string, unknown> = {
|
|
166
|
-
topic: data.topic || "general",
|
|
167
|
-
timestamp,
|
|
168
|
-
date,
|
|
169
|
-
content,
|
|
170
|
-
content_hash: hashContent(content),
|
|
171
|
-
};
|
|
176
|
+
const metadata: Record<string, unknown> = {};
|
|
172
177
|
|
|
173
|
-
// Add type-specific fields
|
|
178
|
+
// Add type-specific fields only (no topic, content, content_hash, date, timestamp)
|
|
174
179
|
switch (event.type) {
|
|
175
180
|
case "knowledge":
|
|
176
181
|
metadata.subtype = data.subtype;
|
|
@@ -255,13 +260,40 @@ function insertEmbedding(
|
|
|
255
260
|
const source = getSourceForEvent(event);
|
|
256
261
|
const data = event.data as Record<string, unknown>;
|
|
257
262
|
const topic = String(data.topic || "");
|
|
263
|
+
const type = extractType(event);
|
|
258
264
|
|
|
259
265
|
const embeddingBlob = serializeEmbedding(embedding);
|
|
260
266
|
|
|
261
267
|
const stmt = db.prepare(`
|
|
262
|
-
INSERT INTO embeddings (doc_id, chunk_idx, source, topic, embedding)
|
|
263
|
-
VALUES (?, 0, ?, ?, ?)
|
|
268
|
+
INSERT INTO embeddings (doc_id, chunk_idx, source, topic, type, embedding)
|
|
269
|
+
VALUES (?, 0, ?, ?, ?, ?)
|
|
264
270
|
`);
|
|
265
271
|
|
|
266
|
-
stmt.run(docId, source, topic, embeddingBlob);
|
|
272
|
+
stmt.run(docId, source, topic, type, embeddingBlob);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extract type value for embeddings partition column
|
|
277
|
+
*/
|
|
278
|
+
function extractType(event: CaptureEvent): string {
|
|
279
|
+
const data = event.data as Record<string, unknown>;
|
|
280
|
+
|
|
281
|
+
switch (event.type) {
|
|
282
|
+
case "knowledge":
|
|
283
|
+
return String(data.subtype || "general");
|
|
284
|
+
case "teaching":
|
|
285
|
+
return "teaching";
|
|
286
|
+
case "observation":
|
|
287
|
+
return String(data.subtype || "pattern");
|
|
288
|
+
case "insight":
|
|
289
|
+
return String(data.subtype || "insight");
|
|
290
|
+
case "learning":
|
|
291
|
+
return "learning";
|
|
292
|
+
case "task":
|
|
293
|
+
return "task";
|
|
294
|
+
case "note":
|
|
295
|
+
return "note";
|
|
296
|
+
default:
|
|
297
|
+
return "general";
|
|
298
|
+
}
|
|
267
299
|
}
|
package/lib/search.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface SearchResult {
|
|
|
15
15
|
title: string;
|
|
16
16
|
content: string;
|
|
17
17
|
metadata: string;
|
|
18
|
+
topic: string;
|
|
18
19
|
rank: number;
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -85,19 +86,16 @@ export function search(
|
|
|
85
86
|
|
|
86
87
|
if (options.type) {
|
|
87
88
|
const types = Array.isArray(options.type) ? options.type : [options.type];
|
|
88
|
-
const typeClauses = types.map(
|
|
89
|
-
() =>
|
|
90
|
-
"(json_extract(metadata, '$.type') = ? OR json_extract(metadata, '$.subtype') = ?)",
|
|
91
|
-
);
|
|
89
|
+
const typeClauses = types.map(() => "type = ?");
|
|
92
90
|
conditions.push(`(${typeClauses.join(" OR ")})`);
|
|
93
91
|
types.forEach((t) => {
|
|
94
|
-
params.push(t
|
|
92
|
+
params.push(t);
|
|
95
93
|
});
|
|
96
94
|
}
|
|
97
95
|
|
|
98
96
|
if (options.since) {
|
|
99
97
|
conditions.push(
|
|
100
|
-
"
|
|
98
|
+
"timestamp IS NOT NULL AND timestamp != '' AND timestamp >= ?",
|
|
101
99
|
);
|
|
102
100
|
params.push(options.since);
|
|
103
101
|
}
|
|
@@ -105,7 +103,7 @@ export function search(
|
|
|
105
103
|
params.push(limit);
|
|
106
104
|
|
|
107
105
|
const sql = `
|
|
108
|
-
SELECT rowid, source, title, snippet(search, 2, '→', '←', '...', 32) as content, metadata, rank
|
|
106
|
+
SELECT rowid, source, title, snippet(search, 2, '→', '←', '...', 32) as content, metadata, topic, rank
|
|
109
107
|
FROM search
|
|
110
108
|
WHERE ${conditions.join(" AND ")}
|
|
111
109
|
ORDER BY rank
|
package/lib/semantic.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface SemanticResult {
|
|
|
18
18
|
title: string;
|
|
19
19
|
content: string;
|
|
20
20
|
metadata: string;
|
|
21
|
+
topic: string;
|
|
21
22
|
distance: number;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -28,21 +29,6 @@ export interface SemanticSearchOptions {
|
|
|
28
29
|
type?: string | string[];
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
* Maps source types to their project/topic field name in metadata JSON.
|
|
33
|
-
* Project-based domains use "project", topic-based domains use "topic".
|
|
34
|
-
*/
|
|
35
|
-
const PROJECT_FIELD: Record<string, string> = {
|
|
36
|
-
commits: "project",
|
|
37
|
-
sessions: "project",
|
|
38
|
-
tasks: "project",
|
|
39
|
-
insights: "topic",
|
|
40
|
-
captures: "topic",
|
|
41
|
-
teachings: "topic",
|
|
42
|
-
learnings: "topic",
|
|
43
|
-
observations: "topic",
|
|
44
|
-
};
|
|
45
|
-
|
|
46
32
|
const MODEL_NAME = "nomic-ai/nomic-embed-text-v1.5";
|
|
47
33
|
const EMBEDDING_DIM = 768;
|
|
48
34
|
|
|
@@ -254,6 +240,7 @@ export async function semanticSearch(
|
|
|
254
240
|
s.title,
|
|
255
241
|
s.content,
|
|
256
242
|
s.metadata,
|
|
243
|
+
s.topic,
|
|
257
244
|
e.distance
|
|
258
245
|
FROM embeddings e
|
|
259
246
|
JOIN search s ON e.doc_id = s.rowid
|
|
@@ -281,6 +268,7 @@ export interface HybridResult {
|
|
|
281
268
|
title: string;
|
|
282
269
|
content: string;
|
|
283
270
|
metadata: string;
|
|
271
|
+
topic: string;
|
|
284
272
|
score: number;
|
|
285
273
|
vectorScore: number;
|
|
286
274
|
textScore: number;
|
|
@@ -367,6 +355,7 @@ export async function hybridSearch(
|
|
|
367
355
|
title: r.title,
|
|
368
356
|
content: r.content,
|
|
369
357
|
metadata: r.metadata,
|
|
358
|
+
topic: r.topic,
|
|
370
359
|
vectorScore,
|
|
371
360
|
textScore: 0,
|
|
372
361
|
score: vectorWeight * vectorScore,
|
|
@@ -393,6 +382,7 @@ export async function hybridSearch(
|
|
|
393
382
|
title: r.title,
|
|
394
383
|
content: r.content,
|
|
395
384
|
metadata: r.metadata,
|
|
385
|
+
topic: r.topic,
|
|
396
386
|
vectorScore: 0,
|
|
397
387
|
textScore,
|
|
398
388
|
score: textWeight * textScore,
|
|
@@ -408,21 +398,6 @@ export async function hybridSearch(
|
|
|
408
398
|
return results;
|
|
409
399
|
}
|
|
410
400
|
|
|
411
|
-
/**
|
|
412
|
-
* Extract project from result metadata
|
|
413
|
-
*/
|
|
414
|
-
function extractProjectFromMetadata(metadata: string, source: string): string {
|
|
415
|
-
const field = PROJECT_FIELD[source];
|
|
416
|
-
if (!field) return "unknown";
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const parsed = JSON.parse(metadata);
|
|
420
|
-
return parsed[field] || "unknown";
|
|
421
|
-
} catch {
|
|
422
|
-
return "unknown";
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
401
|
/**
|
|
427
402
|
* Extract identifier from semantic result
|
|
428
403
|
*/
|
|
@@ -478,7 +453,7 @@ export function formatBriefSearch(results: SemanticResult[]): string {
|
|
|
478
453
|
const lines = [`${source} (${sourceResults.length}):`];
|
|
479
454
|
|
|
480
455
|
sourceResults.forEach((r) => {
|
|
481
|
-
const project =
|
|
456
|
+
const project = r.topic || "unknown";
|
|
482
457
|
const identifier = extractIdentifierFromResult(r);
|
|
483
458
|
const displayText = getDisplayText(r);
|
|
484
459
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voidwire/lore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -19,11 +19,6 @@
|
|
|
19
19
|
"README.md",
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
|
-
"scripts": {
|
|
23
|
-
"build": "tsc --noEmit false --outDir dist --declaration",
|
|
24
|
-
"typecheck": "tsc --noEmit",
|
|
25
|
-
"test": "bun test"
|
|
26
|
-
},
|
|
27
22
|
"keywords": [
|
|
28
23
|
"knowledge",
|
|
29
24
|
"search",
|
|
@@ -48,9 +43,15 @@
|
|
|
48
43
|
"bun": ">=1.0.0"
|
|
49
44
|
},
|
|
50
45
|
"dependencies": {
|
|
51
|
-
"@huggingface/transformers": "^3.2.6"
|
|
46
|
+
"@huggingface/transformers": "^3.2.6",
|
|
47
|
+
"@iarna/toml": "^2.2.5"
|
|
52
48
|
},
|
|
53
49
|
"devDependencies": {
|
|
54
50
|
"bun-types": "1.3.5"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc --noEmit false --outDir dist --declaration",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"test": "bun test"
|
|
55
56
|
}
|
|
56
|
-
}
|
|
57
|
+
}
|