@voidwire/lore 0.9.1 → 1.0.1

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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * lib/indexers/teachings.ts - Teachings indexer
3
+ *
4
+ * Reads log.jsonl and indexes teaching captures.
5
+ * Filters for event=captured AND type=teaching.
6
+ *
7
+ * Source: teachings
8
+ * Topic: data.topic (AI-written)
9
+ * Type: teaching (fixed)
10
+ * Timestamp: event timestamp
11
+ */
12
+
13
+ import { readFileSync, existsSync } from "fs";
14
+ import type { IndexerContext } from "../indexer";
15
+
16
+ export async function indexTeachings(ctx: IndexerContext): Promise<void> {
17
+ const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
+ if (!existsSync(logPath)) {
19
+ console.log("No log.jsonl found, skipping teachings");
20
+ return;
21
+ }
22
+
23
+ const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
24
+
25
+ for (const line of lines) {
26
+ try {
27
+ const event = JSON.parse(line);
28
+ if (event.event !== "captured" || event.type !== "teaching") continue;
29
+
30
+ const topic = event.data?.topic || "general";
31
+ const content = event.data?.content || "";
32
+ const confidence = event.data?.confidence;
33
+
34
+ if (!content) continue;
35
+
36
+ const metadata: Record<string, unknown> = {};
37
+ if (confidence) metadata.confidence = confidence;
38
+
39
+ ctx.insert({
40
+ source: "teachings",
41
+ title: `[teaching] ${topic}`,
42
+ content,
43
+ topic,
44
+ type: "teaching",
45
+ timestamp: event.timestamp,
46
+ metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
47
+ });
48
+ } catch (e) {
49
+ continue;
50
+ }
51
+ }
52
+ }
package/lib/info.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import { Database } from "bun:sqlite";
9
- import { homedir } from "os";
10
9
  import { existsSync } from "fs";
10
+ import { getDatabasePath } from "./db.js";
11
11
  import { projects as getProjects } from "./projects.js";
12
12
 
13
13
  export interface SourceInfo {
@@ -22,10 +22,6 @@ export interface InfoOutput {
22
22
  total_entries: number;
23
23
  }
24
24
 
25
- function getDatabasePath(): string {
26
- return `${homedir()}/.local/share/lore/lore.db`;
27
- }
28
-
29
25
  /**
30
26
  * Get info about indexed sources
31
27
  *
@@ -62,11 +58,11 @@ export function info(): InfoOutput {
62
58
  const totalResult = totalStmt.get() as { total: number };
63
59
  const total_entries = totalResult?.total ?? 0;
64
60
 
65
- // Get last indexed timestamp from metadata
61
+ // Get last indexed timestamp from column
66
62
  const tsStmt = db.prepare(`
67
- SELECT MAX(json_extract(metadata, '$.timestamp')) as ts
63
+ SELECT MAX(timestamp) as ts
68
64
  FROM search
69
- WHERE json_extract(metadata, '$.timestamp') IS NOT NULL
65
+ WHERE timestamp IS NOT NULL AND timestamp != ''
70
66
  `);
71
67
  const tsResult = tsStmt.get() as { ts: string | null };
72
68
  const last_indexed = tsResult?.ts ?? new Date().toISOString();
package/lib/list.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import { Database } from "bun:sqlite";
9
- import { homedir } from "os";
10
9
  import { existsSync } from "fs";
10
+ import { getDatabasePath } from "./db.js";
11
11
 
12
12
  // Source types - data sources that can be listed
13
13
  export type Source =
@@ -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
 
@@ -96,14 +85,11 @@ export interface ListResult {
96
85
  count: number;
97
86
  }
98
87
 
99
- // Database path following XDG spec
100
- function getDatabasePath(): string {
101
- return `${homedir()}/.local/share/lore/lore.db`;
102
- }
103
-
104
88
  interface RawRow {
105
89
  title: string;
106
90
  content: string;
91
+ topic: string;
92
+ type: string;
107
93
  metadata: string;
108
94
  }
109
95
 
@@ -117,26 +103,24 @@ function queryBySource(
117
103
  project?: string,
118
104
  type?: string,
119
105
  ): ListEntry[] {
120
- let sql = "SELECT title, content, metadata FROM search WHERE source = ?";
106
+ let sql =
107
+ "SELECT title, content, topic, type, metadata FROM search WHERE source = ?";
121
108
  const params: (string | number)[] = [source];
122
109
 
123
- // Add project filter if provided and source has a project field
110
+ // Add project filter if provided uses topic column directly
124
111
  if (project) {
125
- const field = PROJECT_FIELD[source];
126
- if (field) {
127
- sql += ` AND json_extract(metadata, '$.${field}') = ?`;
128
- params.push(project);
129
- }
112
+ sql += " AND topic = ?";
113
+ params.push(project);
130
114
  }
131
115
 
132
- // Add type filter if provided (captures source only)
133
- if (type && source === "captures") {
134
- sql += ` AND json_extract(metadata, '$.type') = ?`;
116
+ // Add type filter if provided uses type column directly
117
+ if (type) {
118
+ sql += " AND type = ?";
135
119
  params.push(type);
136
120
  }
137
121
 
138
122
  // Order by timestamp descending (most recent first)
139
- sql += " ORDER BY json_extract(metadata, '$.timestamp') DESC";
123
+ sql += " ORDER BY timestamp DESC";
140
124
 
141
125
  if (limit) {
142
126
  sql += " LIMIT ?";
@@ -149,6 +133,8 @@ function queryBySource(
149
133
  return rows.map((row) => ({
150
134
  title: row.title,
151
135
  content: row.content,
136
+ topic: row.topic,
137
+ type: row.type,
152
138
  metadata: JSON.parse(row.metadata || "{}"),
153
139
  }));
154
140
  }
@@ -163,10 +149,10 @@ function queryPersonalType(
163
149
  ): ListEntry[] {
164
150
  // Filter by type in SQL, not JS - avoids LIMIT truncation bug
165
151
  let sql = `
166
- SELECT title, content, metadata FROM search
152
+ SELECT title, content, topic, type, metadata FROM search
167
153
  WHERE source = 'personal'
168
- AND json_extract(metadata, '$.type') = ?
169
- ORDER BY json_extract(metadata, '$.timestamp') DESC
154
+ AND type = ?
155
+ ORDER BY timestamp DESC
170
156
  `;
171
157
  const params: (string | number)[] = [type];
172
158
 
@@ -181,6 +167,8 @@ function queryPersonalType(
181
167
  return rows.map((row) => ({
182
168
  title: row.title,
183
169
  content: row.content,
170
+ topic: row.topic,
171
+ type: row.type,
184
172
  metadata: JSON.parse(row.metadata || "{}"),
185
173
  }));
186
174
  }
@@ -203,7 +191,7 @@ export function list(source: Source, options: ListOptions = {}): ListResult {
203
191
  const dbPath = getDatabasePath();
204
192
 
205
193
  if (!existsSync(dbPath)) {
206
- throw new Error(`Database not found: ${dbPath}. Run lore-index-all first.`);
194
+ throw new Error(`Database not found: ${dbPath}. Run lore-db-init first.`);
207
195
  }
208
196
 
209
197
  const db = new Database(dbPath, { readonly: true });
@@ -243,12 +231,10 @@ export function listSources(): Source[] {
243
231
  }
244
232
 
245
233
  /**
246
- * Extract project name from entry metadata
234
+ * Extract project name from entry
247
235
  */
248
- function extractProjectFromEntry(entry: ListEntry, source: string): string {
249
- const field = PROJECT_FIELD[source];
250
- if (!field) return "unknown";
251
- return (entry.metadata[field] as string) || "unknown";
236
+ function extractProjectFromEntry(entry: ListEntry, _source: string): string {
237
+ return entry.topic || "unknown";
252
238
  }
253
239
 
254
240
  /**
package/lib/projects.ts CHANGED
@@ -1,29 +1,24 @@
1
1
  /**
2
2
  * lib/projects.ts - List all known projects across sources
3
3
  *
4
- * Queries distinct project values from metadata fields, handling
5
- * different field names per source type.
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
- import { homedir } from "os";
10
9
  import { existsSync } from "fs";
11
-
12
- // Project-based domains use "project", topic-based domains use "topic"
13
- const PROJECT_FIELD: Record<string, string> = {
14
- commits: "project",
15
- sessions: "project",
16
- tasks: "project",
17
- insights: "topic",
18
- captures: "topic",
19
- teachings: "topic",
20
- learnings: "topic",
21
- observations: "topic",
22
- };
23
-
24
- function getDatabasePath(): string {
25
- return `${homedir()}/.local/share/lore/lore.db`;
26
- }
10
+ import { getDatabasePath } from "./db.js";
11
+
12
+ const PROJECT_SOURCES = [
13
+ "commits",
14
+ "sessions",
15
+ "flux",
16
+ "insights",
17
+ "captures",
18
+ "teachings",
19
+ "learnings",
20
+ "observations",
21
+ ];
27
22
 
28
23
  /**
29
24
  * Get all unique project names across sources
@@ -40,24 +35,20 @@ export function projects(): string[] {
40
35
  const db = new Database(dbPath, { readonly: true });
41
36
 
42
37
  try {
43
- const allProjects = new Set<string>();
44
-
45
- for (const [source, field] of Object.entries(PROJECT_FIELD)) {
46
- const stmt = db.prepare(`
47
- SELECT DISTINCT json_extract(metadata, '$.${field}') as proj
48
- FROM search
49
- WHERE source = ? AND json_extract(metadata, '$.${field}') IS NOT NULL
50
- `);
51
- const results = stmt.all(source) as { proj: string | null }[];
52
-
53
- for (const r of results) {
54
- if (r.proj) {
55
- allProjects.add(r.proj);
56
- }
57
- }
58
- }
59
-
60
- return Array.from(allProjects).sort();
38
+ const placeholders = PROJECT_SOURCES.map(() => "?").join(", ");
39
+ const stmt = db.prepare(`
40
+ SELECT DISTINCT topic
41
+ FROM search
42
+ WHERE source IN (${placeholders})
43
+ AND topic IS NOT NULL
44
+ AND topic != ''
45
+ `);
46
+ const results = stmt.all(...PROJECT_SOURCES) as { topic: string }[];
47
+
48
+ return results
49
+ .map((r) => r.topic)
50
+ .filter(Boolean)
51
+ .sort();
61
52
  } finally {
62
53
  db.close();
63
54
  }
package/lib/realtime.ts CHANGED
@@ -23,12 +23,7 @@ import {
23
23
  EMBEDDING_DIM,
24
24
  serializeEmbedding,
25
25
  } from "./semantic.js";
26
- import {
27
- hashContent,
28
- getCachedEmbedding,
29
- cacheEmbedding,
30
- getMissingHashes,
31
- } from "./cache.js";
26
+ import { hashContent, getCachedEmbedding, cacheEmbedding } from "./cache.js";
32
27
  import type { CaptureEvent } from "./capture.js";
33
28
 
34
29
  /**
@@ -89,13 +84,23 @@ function insertSearchEntry(db: Database, event: CaptureEvent): number {
89
84
  const metadata = buildMetadata(event);
90
85
  const data = event.data as Record<string, unknown>;
91
86
  const topic = String(data.topic || "");
87
+ const type = extractType(event);
88
+ const timestamp = event.timestamp || new Date().toISOString();
92
89
 
93
90
  const stmt = db.prepare(`
94
- INSERT INTO search (source, title, content, metadata, topic)
95
- VALUES (?, ?, ?, ?, ?)
91
+ INSERT INTO search (source, title, content, metadata, topic, type, timestamp)
92
+ VALUES (?, ?, ?, ?, ?, ?, ?)
96
93
  `);
97
94
 
98
- const result = stmt.run(source, title, content, metadata, topic);
95
+ const result = stmt.run(
96
+ source,
97
+ title,
98
+ content,
99
+ metadata,
100
+ topic,
101
+ type,
102
+ timestamp,
103
+ );
99
104
  return Number(result.lastInsertRowid);
100
105
  }
101
106
 
@@ -163,31 +168,19 @@ function getContentForEmbedding(event: CaptureEvent): string {
163
168
  */
164
169
  function buildMetadata(event: CaptureEvent): string {
165
170
  const data = event.data as Record<string, unknown>;
166
- const timestamp = event.timestamp;
167
- const date = timestamp ? timestamp.substring(0, 10) : "";
168
-
169
- const content = getContentForEmbedding(event);
170
- const metadata: Record<string, unknown> = {
171
- topic: data.topic || "general",
172
- timestamp,
173
- date,
174
- content,
175
- content_hash: hashContent(content),
176
- };
171
+ const metadata: Record<string, unknown> = {};
177
172
 
178
- // Add type-specific fields
173
+ // Add type-specific fields only (no topic, content, content_hash, date, timestamp)
179
174
  switch (event.type) {
180
175
  case "knowledge":
181
176
  metadata.subtype = data.subtype;
182
177
  break;
183
178
  case "teaching":
184
179
  metadata.confidence = data.confidence;
185
- metadata.capture_source = data.source || "manual";
186
180
  break;
187
181
  case "observation":
188
182
  metadata.subtype = data.subtype;
189
183
  metadata.confidence = data.confidence;
190
- metadata.capture_source = data.source || "auto";
191
184
  break;
192
185
  case "insight":
193
186
  metadata.subtype = data.subtype;
package/lib/search.ts CHANGED
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import { Database } from "bun:sqlite";
9
- import { homedir } from "os";
10
9
  import { existsSync } from "fs";
10
+ import { getDatabasePath } from "./db.js";
11
11
 
12
12
  export interface SearchResult {
13
13
  rowid: number;
@@ -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
 
@@ -25,10 +26,6 @@ export interface SearchOptions {
25
26
  type?: string | string[];
26
27
  }
27
28
 
28
- function getDatabasePath(): string {
29
- return `${homedir()}/.local/share/lore/lore.db`;
30
- }
31
-
32
29
  /**
33
30
  * Escape a query for safe FTS5 MATCH
34
31
  * Wraps terms in double quotes to prevent FTS5 syntax interpretation
@@ -85,19 +82,16 @@ export function search(
85
82
 
86
83
  if (options.type) {
87
84
  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
- );
85
+ const typeClauses = types.map(() => "type = ?");
92
86
  conditions.push(`(${typeClauses.join(" OR ")})`);
93
87
  types.forEach((t) => {
94
- params.push(t, t);
88
+ params.push(t);
95
89
  });
96
90
  }
97
91
 
98
92
  if (options.since) {
99
93
  conditions.push(
100
- "json_extract(metadata, '$.date') IS NOT NULL AND json_extract(metadata, '$.date') != 'unknown' AND json_extract(metadata, '$.date') >= ?",
94
+ "timestamp IS NOT NULL AND timestamp != '' AND timestamp >= ?",
101
95
  );
102
96
  params.push(options.since);
103
97
  }
@@ -105,7 +99,7 @@ export function search(
105
99
  params.push(limit);
106
100
 
107
101
  const sql = `
108
- SELECT rowid, source, title, snippet(search, 2, '→', '←', '...', 32) as content, metadata, rank
102
+ SELECT rowid, source, title, snippet(search, 2, '→', '←', '...', 32) as content, metadata, topic, rank
109
103
  FROM search
110
104
  WHERE ${conditions.join(" AND ")}
111
105
  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 = extractProjectFromMetadata(r.metadata, r.source);
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.9.1",
3
+ "version": "1.0.1",
4
4
  "description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -43,7 +43,8 @@
43
43
  "bun": ">=1.0.0"
44
44
  },
45
45
  "dependencies": {
46
- "@huggingface/transformers": "^3.2.6"
46
+ "@huggingface/transformers": "^3.2.6",
47
+ "@iarna/toml": "^2.2.5"
47
48
  },
48
49
  "devDependencies": {
49
50
  "bun-types": "1.3.5"