@voidwire/lore 1.6.2 → 1.7.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/index.ts CHANGED
@@ -67,7 +67,6 @@ export {
67
67
  captureNote,
68
68
  captureTeaching,
69
69
  captureInsight,
70
- captureLearning,
71
70
  captureObservation,
72
71
  type CaptureResult,
73
72
  type KnowledgeInput,
@@ -77,7 +76,6 @@ export {
77
76
  type TeachingInput,
78
77
  type InsightInput,
79
78
  type InsightType,
80
- type LearningInput,
81
79
  type ObservationInput,
82
80
  type ObservationSubtype,
83
81
  type ObservationConfidence,
package/lib/capture.ts CHANGED
@@ -74,13 +74,6 @@ export interface InsightInput {
74
74
  source: "auto";
75
75
  }
76
76
 
77
- export interface LearningInput {
78
- topic: string; // "spanish", "guitar", "kubernetes" - the learning topic
79
- persona: string; // "marcus", "elena", etc.
80
- content: string; // "Covered verb conjugations, struggles with subjunctive"
81
- session_summary?: string; // Longer form session notes
82
- }
83
-
84
77
  export type ObservationSubtype =
85
78
  | "term"
86
79
  | "style"
@@ -164,18 +157,6 @@ interface InsightEvent {
164
157
  };
165
158
  }
166
159
 
167
- interface LearningEvent {
168
- event: "captured";
169
- type: "learning";
170
- timestamp: string;
171
- data: {
172
- topic: string; // Learning topic (spanish, guitar, etc.)
173
- persona: string;
174
- content: string;
175
- session_summary?: string;
176
- };
177
- }
178
-
179
160
  interface ObservationEvent {
180
161
  event: "captured";
181
162
  type: "observation";
@@ -195,7 +176,6 @@ type CaptureEvent =
195
176
  | NoteEvent
196
177
  | TeachingEvent
197
178
  | InsightEvent
198
- | LearningEvent
199
179
  | ObservationEvent;
200
180
 
201
181
  function getLogPath(): string {
@@ -372,32 +352,6 @@ export function captureInsight(input: InsightInput): CaptureResult {
372
352
  return writeEvent(event);
373
353
  }
374
354
 
375
- /**
376
- * Capture a learning session progress
377
- */
378
- export function captureLearning(input: LearningInput): CaptureResult {
379
- if (!input.topic || !input.persona || !input.content) {
380
- return {
381
- success: false,
382
- error: "Missing required fields: topic, persona, content",
383
- };
384
- }
385
-
386
- const event: LearningEvent = {
387
- event: "captured",
388
- type: "learning",
389
- timestamp: "",
390
- data: {
391
- topic: input.topic,
392
- persona: input.persona,
393
- content: input.content,
394
- session_summary: input.session_summary,
395
- },
396
- };
397
-
398
- return writeEvent(event);
399
- }
400
-
401
355
  const VALID_OBSERVATION_SUBTYPES: ObservationSubtype[] = [
402
356
  "term",
403
357
  "style",
@@ -16,7 +16,9 @@
16
16
 
17
17
  import { hybridSearch, type HybridResult } from "./semantic.js";
18
18
  import { PURGEABLE_SOURCES } from "./purge.js";
19
+ import { complete } from "@voidwire/llm-core";
19
20
  import type { CaptureEvent } from "./capture.js";
21
+ import { getSourceForEvent } from "./source-map.js";
20
22
 
21
23
  // ─── Types ──────────────────────────────────────────────────────────────────
22
24
 
@@ -38,11 +40,8 @@ export interface ContradictionDecision {
38
40
 
39
41
  // ─── Constants ──────────────────────────────────────────────────────────────
40
42
 
41
- const MLX_URL = "http://localhost:8080/v1/chat/completions";
42
- const MLX_MODEL = "mlx-community/Qwen2.5-7B-Instruct-4bit";
43
- const MLX_TIMEOUT_MS = 1500;
44
-
45
43
  const CANDIDATE_LIMIT = 5;
44
+ const LLM_SERVICE = "mlx";
46
45
 
47
46
  // Sources eligible for contradiction checking (same as purgeable)
48
47
  const CONTRADICTION_SOURCES = new Set<string>(PURGEABLE_SOURCES);
@@ -126,33 +125,17 @@ If DELETE, reply: DELETE <rowid>
126
125
  Otherwise reply: ADD or NOOP`;
127
126
 
128
127
  try {
129
- const resp = await fetch(MLX_URL, {
130
- method: "POST",
131
- headers: { "Content-Type": "application/json" },
132
- body: JSON.stringify({
133
- model: MLX_MODEL,
134
- messages: [
135
- { role: "system", content: systemPrompt },
136
- { role: "user", content: userPrompt },
137
- ],
138
- max_tokens: 20,
139
- temperature: 0,
140
- }),
141
- signal: AbortSignal.timeout(MLX_TIMEOUT_MS),
128
+ const result = await complete({
129
+ service: LLM_SERVICE,
130
+ prompt: userPrompt,
131
+ systemPrompt,
132
+ maxTokens: 20,
133
+ temperature: 0,
142
134
  });
143
135
 
144
- if (!resp.ok) {
145
- return { action: "ADD" };
146
- }
147
-
148
- const json = (await resp.json()) as {
149
- choices?: Array<{ message?: { content?: string } }>;
150
- };
151
-
152
- const raw = json.choices?.[0]?.message?.content?.trim() || "";
153
- return parseClassification(raw);
136
+ return parseClassification(result.text);
154
137
  } catch {
155
- // Timeout, network error, or model unavailable — fail open
138
+ // Network error, model unavailable — fail open
156
139
  return { action: "ADD" };
157
140
  }
158
141
  }
@@ -187,27 +170,3 @@ function parseClassification(raw: string): ContradictionResult {
187
170
  // Unparseable — default to ADD
188
171
  return { action: "ADD" };
189
172
  }
190
-
191
- /**
192
- * Map event type to source name (mirrors realtime.ts getSourceForEvent).
193
- */
194
- function getSourceForEvent(event: CaptureEvent): string {
195
- switch (event.type) {
196
- case "knowledge":
197
- return "captures";
198
- case "teaching":
199
- return "teachings";
200
- case "observation":
201
- return "observations";
202
- case "insight":
203
- return "insights";
204
- case "learning":
205
- return "learnings";
206
- case "task":
207
- return "flux";
208
- case "note":
209
- return "captures";
210
- default:
211
- return "captures";
212
- }
213
- }
@@ -7,7 +7,6 @@
7
7
 
8
8
  import type { IndexerFunction } from "../indexer";
9
9
  import { indexEvents } from "./events";
10
- import { indexLearnings } from "./learnings";
11
10
  import { indexReadmes } from "./readmes";
12
11
  import { indexDevelopment } from "./development";
13
12
  import { indexCaptures } from "./captures";
@@ -24,7 +23,6 @@ import { indexPersonal } from "./personal";
24
23
 
25
24
  export const indexers: Record<string, IndexerFunction> = {
26
25
  events: indexEvents,
27
- learnings: indexLearnings,
28
26
  readmes: indexReadmes,
29
27
  development: indexDevelopment,
30
28
  captures: indexCaptures,
package/lib/list.ts CHANGED
@@ -29,7 +29,6 @@ export type Source =
29
29
  | "teachings"
30
30
  | "sessions"
31
31
  | "insights"
32
- | "learnings"
33
32
  | "observations";
34
33
 
35
34
  export const SOURCES: Source[] = [
@@ -51,7 +50,6 @@ export const SOURCES: Source[] = [
51
50
  "teachings",
52
51
  "sessions",
53
52
  "insights",
54
- "learnings",
55
53
  "observations",
56
54
  ];
57
55
 
package/lib/projects.ts CHANGED
@@ -16,7 +16,6 @@ const PROJECT_SOURCES = [
16
16
  "insights",
17
17
  "captures",
18
18
  "teachings",
19
- "learnings",
20
19
  "observations",
21
20
  ];
22
21
 
package/lib/purge.ts CHANGED
@@ -9,7 +9,6 @@
9
9
  * deleteEntries(matches.map(m => m.rowid));
10
10
  */
11
11
 
12
- import { Database } from "bun:sqlite";
13
12
  import {
14
13
  existsSync,
15
14
  readFileSync,
@@ -18,7 +17,7 @@ import {
18
17
  unlinkSync,
19
18
  } from "fs";
20
19
  import { join } from "path";
21
- import { getDatabasePath, openDatabase } from "./db.js";
20
+ import { openDatabase } from "./db.js";
22
21
  import { getConfig } from "./config.js";
23
22
 
24
23
  // Only these sources can be purged — indexed sources (blogs, commits, etc.) are never purgeable
@@ -101,25 +100,9 @@ export function deleteEntries(
101
100
  return { deleted: 0, rowids: [], logEntriesRemoved: 0 };
102
101
  }
103
102
 
104
- // Open DB directly for read-write (matches realtime.ts pattern —
105
- // openDatabase(false) triggers SQLITE_MISUSE with custom_sqlite)
106
- const dbPath = getDatabasePath();
107
- if (!existsSync(dbPath)) {
108
- throw new Error(`Database not found: ${dbPath}. Run lore-db-init first.`);
109
- }
110
-
111
- const db = new Database(dbPath);
103
+ const db = openDatabase(false);
112
104
 
113
105
  try {
114
- // Load sqlite-vec extension for embeddings table access
115
- const vecPath = process.env.SQLITE_VEC_PATH;
116
- if (!vecPath) {
117
- throw new Error(
118
- 'SQLITE_VEC_PATH not set. Get path with: python3 -c "import sqlite_vec; print(sqlite_vec.loadable_path())"',
119
- );
120
- }
121
- db.loadExtension(vecPath);
122
-
123
106
  const deleteSearch = db.prepare("DELETE FROM search WHERE rowid = ?");
124
107
  const deleteEmbedding = db.prepare(
125
108
  "DELETE FROM embeddings WHERE doc_id = ?",
package/lib/realtime.ts CHANGED
@@ -15,16 +15,16 @@
15
15
  */
16
16
 
17
17
  import { Database } from "bun:sqlite";
18
- import { existsSync } from "fs";
19
18
  import {
20
19
  embedDocuments,
21
- getDatabasePath,
22
20
  MODEL_NAME,
23
21
  EMBEDDING_DIM,
24
22
  serializeEmbedding,
25
23
  } from "./semantic.js";
24
+ import { openDatabase } from "./db.js";
26
25
  import { hashContent, getCachedEmbedding, cacheEmbedding } from "./cache.js";
27
26
  import type { CaptureEvent } from "./capture.js";
27
+ import { getSourceForEvent } from "./source-map.js";
28
28
  import {
29
29
  isContradictionCheckable,
30
30
  findCandidates,
@@ -44,23 +44,9 @@ export async function indexAndEmbed(
44
44
  ): Promise<ContradictionDecision[]> {
45
45
  if (events.length === 0) return [];
46
46
 
47
- const dbPath = getDatabasePath();
48
- if (!existsSync(dbPath)) {
49
- throw new Error(`Database not found: ${dbPath}. Run lore-db-init first.`);
50
- }
51
-
52
- const db = new Database(dbPath);
47
+ const db = openDatabase(false);
53
48
 
54
49
  try {
55
- // Load sqlite-vec extension for embeddings table
56
- const vecPath = process.env.SQLITE_VEC_PATH;
57
- if (!vecPath) {
58
- throw new Error(
59
- 'SQLITE_VEC_PATH not set. Get path with: python3 -c "import sqlite_vec; print(sqlite_vec.loadable_path())"',
60
- );
61
- }
62
- db.loadExtension(vecPath);
63
-
64
50
  // 0. Contradiction detection — filter events before insert
65
51
  // For purgeable sources, check if the new event contradicts or
66
52
  // duplicates existing entries. NOOP skips the event, DELETE+ADD
@@ -137,9 +123,9 @@ export async function indexAndEmbed(
137
123
  function insertSearchEntry(db: Database, event: CaptureEvent): number {
138
124
  const source = getSourceForEvent(event);
139
125
  const title = buildTitle(event);
140
- const content = getContentForEmbedding(event);
141
- const metadata = buildMetadata(event);
142
126
  const data = event.data as Record<string, unknown>;
127
+ const content = String(data.content || data.text || "");
128
+ const metadata = buildMetadata(event);
143
129
  const topic = String(data.topic || "");
144
130
  const type = extractType(event);
145
131
  const timestamp = event.timestamp || new Date().toISOString();
@@ -171,30 +157,6 @@ function deleteSearchAndEmbedding(db: Database, rowid: number): void {
171
157
  db.prepare("DELETE FROM embeddings WHERE doc_id = ?").run(rowid);
172
158
  }
173
159
 
174
- /**
175
- * Map event type to source name used in search table
176
- */
177
- function getSourceForEvent(event: CaptureEvent): string {
178
- switch (event.type) {
179
- case "knowledge":
180
- return "captures";
181
- case "teaching":
182
- return "teachings";
183
- case "observation":
184
- return "observations";
185
- case "insight":
186
- return "insights";
187
- case "learning":
188
- return "learnings";
189
- case "task":
190
- return "flux";
191
- case "note":
192
- return "captures";
193
- default:
194
- return "captures";
195
- }
196
- }
197
-
198
160
  /**
199
161
  * Build title for FTS5 entry (type is a first-class column, not a title prefix)
200
162
  */
@@ -210,24 +172,26 @@ function buildTitle(event: CaptureEvent): string {
210
172
  return `${data.topic || "general"}`;
211
173
  case "insight":
212
174
  return `${data.topic || "general"}`;
213
- case "learning":
214
- return `${data.topic || "general"}`;
215
175
  case "task":
216
176
  return `${data.topic || "general"}: ${data.name || "untitled"}`;
217
177
  case "note":
218
178
  return `${data.topic || "general"}`;
179
+ default:
180
+ return `${data.topic || "general"}`;
219
181
  }
220
182
  }
221
183
 
222
184
  /**
223
185
  * Extract content for embedding from event
224
- * Concatenates topic+content for richer embeddings (matches lore-embed-all)
186
+ * Concatenates type+topic+content for richer embeddings (matches lore-embed-all)
225
187
  */
226
188
  function getContentForEmbedding(event: CaptureEvent): string {
227
189
  const data = event.data as Record<string, unknown>;
228
190
  const content = String(data.content || data.text || "");
229
191
  const topic = String(data.topic || "").trim();
230
- return topic ? `${topic} ${content}`.trim() : content;
192
+ const type = extractType(event);
193
+ const parts = [type, topic, content].filter(Boolean);
194
+ return parts.join(" ").trim() || content;
231
195
  }
232
196
 
233
197
  /**
@@ -253,9 +217,6 @@ function buildMetadata(event: CaptureEvent): string {
253
217
  metadata.subtype = data.subtype;
254
218
  metadata.session_id = data.session_id;
255
219
  break;
256
- case "learning":
257
- metadata.persona = data.persona;
258
- break;
259
220
  case "task":
260
221
  metadata.name = data.name;
261
222
  metadata.problem = data.problem;
@@ -349,8 +310,6 @@ function extractType(event: CaptureEvent): string {
349
310
  return String(data.subtype || "pattern");
350
311
  case "insight":
351
312
  return String(data.subtype || "insight");
352
- case "learning":
353
- return "learning";
354
313
  case "task":
355
314
  return "task";
356
315
  case "note":
@@ -0,0 +1,30 @@
1
+ /**
2
+ * lib/source-map.ts - Shared event-to-source mapping
3
+ *
4
+ * Maps capture event types to their source names in the search table.
5
+ * Single source of truth — used by realtime.ts and contradiction.ts.
6
+ */
7
+
8
+ import type { CaptureEvent } from "./capture.js";
9
+
10
+ /**
11
+ * Map event type to source name used in search table
12
+ */
13
+ export function getSourceForEvent(event: CaptureEvent): string {
14
+ switch (event.type) {
15
+ case "knowledge":
16
+ return "captures";
17
+ case "teaching":
18
+ return "teachings";
19
+ case "observation":
20
+ return "observations";
21
+ case "insight":
22
+ return "insights";
23
+ case "task":
24
+ return "flux";
25
+ case "note":
26
+ return "captures";
27
+ default:
28
+ return "captures";
29
+ }
30
+ }
package/lib/types.ts CHANGED
@@ -18,6 +18,14 @@ export enum LoreType {
18
18
  Task = "task",
19
19
  Todo = "todo",
20
20
  Idea = "idea",
21
+ Knowledge = "knowledge",
22
+ Insight = "insight",
23
+ Summary = "summary",
24
+ Observation = "observation",
25
+ Context = "context",
26
+ Completion = "completion",
27
+ Problem = "problem",
28
+ Tool = "tool",
21
29
  }
22
30
 
23
31
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidwire/lore",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -1,57 +0,0 @@
1
- /**
2
- * lib/indexers/learnings.ts - Learnings indexer
3
- *
4
- * Reads log.jsonl and indexes learning captures.
5
- * Filters for event=captured AND type=learning.
6
- *
7
- * Source: learnings
8
- * Topic: data.topic
9
- * Type: (empty)
10
- * Timestamp: event timestamp
11
- */
12
-
13
- import { readFileSync } from "fs";
14
- import { checkPath, type IndexerContext } from "../indexer";
15
-
16
- export async function indexLearnings(ctx: IndexerContext): Promise<void> {
17
- const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
- if (
19
- !checkPath(
20
- "learnings",
21
- "log.jsonl",
22
- logPath,
23
- "populated by Sable session hooks",
24
- )
25
- )
26
- return;
27
-
28
- const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
29
-
30
- for (const line of lines) {
31
- try {
32
- const event = JSON.parse(line);
33
- if (event.event !== "captured" || event.type !== "learning") continue;
34
-
35
- const topic = event.data?.topic || "general";
36
- const content = event.data?.content || "";
37
- const persona = event.data?.persona;
38
-
39
- if (!content) continue;
40
-
41
- const metadata: Record<string, unknown> = {};
42
- if (persona) metadata.persona = persona;
43
-
44
- ctx.insert({
45
- source: "learnings",
46
- title: topic,
47
- content,
48
- topic,
49
- timestamp: event.timestamp || "",
50
- metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
51
- });
52
- } catch {
53
- // Skip malformed JSON
54
- continue;
55
- }
56
- }
57
- }