@voidwire/lore 0.9.1 → 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/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 = "SELECT title, content, metadata FROM search WHERE source = ?";
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 and source has a project field
115
+ // Add project filter if provided uses topic column directly
124
116
  if (project) {
125
- const field = PROJECT_FIELD[source];
126
- if (field) {
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 (captures source only)
133
- if (type && source === "captures") {
134
- sql += ` AND json_extract(metadata, '$.type') = ?`;
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 json_extract(metadata, '$.timestamp') DESC";
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 json_extract(metadata, '$.type') = ?
169
- ORDER BY json_extract(metadata, '$.timestamp') DESC
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 metadata
239
+ * Extract project name from entry
247
240
  */
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";
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 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
9
  import { homedir } from "os";
10
10
  import { existsSync } from "fs";
11
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
- };
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 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();
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
@@ -89,13 +89,23 @@ function insertSearchEntry(db: Database, event: CaptureEvent): number {
89
89
  const metadata = buildMetadata(event);
90
90
  const data = event.data as Record<string, unknown>;
91
91
  const topic = String(data.topic || "");
92
+ const type = extractType(event);
93
+ const timestamp = event.timestamp || new Date().toISOString();
92
94
 
93
95
  const stmt = db.prepare(`
94
- INSERT INTO search (source, title, content, metadata, topic)
95
- VALUES (?, ?, ?, ?, ?)
96
+ INSERT INTO search (source, title, content, metadata, topic, type, timestamp)
97
+ VALUES (?, ?, ?, ?, ?, ?, ?)
96
98
  `);
97
99
 
98
- const result = stmt.run(source, title, content, metadata, topic);
100
+ const result = stmt.run(
101
+ source,
102
+ title,
103
+ content,
104
+ metadata,
105
+ topic,
106
+ type,
107
+ timestamp,
108
+ );
99
109
  return Number(result.lastInsertRowid);
100
110
  }
101
111
 
@@ -163,19 +173,9 @@ function getContentForEmbedding(event: CaptureEvent): string {
163
173
  */
164
174
  function buildMetadata(event: CaptureEvent): string {
165
175
  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
- };
176
+ const metadata: Record<string, unknown> = {};
177
177
 
178
- // Add type-specific fields
178
+ // Add type-specific fields only (no topic, content, content_hash, date, timestamp)
179
179
  switch (event.type) {
180
180
  case "knowledge":
181
181
  metadata.subtype = data.subtype;
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, t);
92
+ params.push(t);
95
93
  });
96
94
  }
97
95
 
98
96
  if (options.since) {
99
97
  conditions.push(
100
- "json_extract(metadata, '$.date') IS NOT NULL AND json_extract(metadata, '$.date') != 'unknown' AND json_extract(metadata, '$.date') >= ?",
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 = 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.0",
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"