@voidwire/lore 1.0.6 → 1.0.8

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
  }
package/lib/indexer.ts CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { Database } from "bun:sqlite";
20
20
  import { createHash } from "crypto";
21
+ import { existsSync } from "fs";
21
22
  import { getConfig, type LoreConfig } from "./config";
22
23
 
23
24
  export interface IndexEntry {
@@ -39,6 +40,35 @@ export interface IndexerContext {
39
40
 
40
41
  export type IndexerFunction = (ctx: IndexerContext) => Promise<void>;
41
42
 
43
+ /**
44
+ * Check if a path is configured and exists on disk.
45
+ * Logs a specific reason when the check fails:
46
+ * - "not configured" when path is undefined
47
+ * - "not found: /path" or "not found — hint" when path doesn't exist
48
+ *
49
+ * Returns true (with type narrowing) if path exists.
50
+ */
51
+ export function checkPath(
52
+ source: string,
53
+ name: string,
54
+ path: string | undefined,
55
+ hint?: string,
56
+ ): path is string {
57
+ if (!path) {
58
+ console.log(`${source}: ${name} not configured`);
59
+ return false;
60
+ }
61
+ if (!existsSync(path)) {
62
+ if (hint) {
63
+ console.log(`${source}: ${name} not found — ${hint}`);
64
+ } else {
65
+ console.log(`${source}: ${name} not found: ${path}`);
66
+ }
67
+ return false;
68
+ }
69
+ return true;
70
+ }
71
+
42
72
  /**
43
73
  * Content chunking with overlap.
44
74
  * Splits content at sentence boundaries when possible.
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { readdirSync, readFileSync, statSync, existsSync } from "fs";
15
15
  import { join, basename } from "path";
16
- import type { IndexerContext } from "../indexer";
16
+ import { checkPath, type IndexerContext } from "../indexer";
17
17
 
18
18
  function walkMarkdownFiles(dir: string, files: string[] = []): string[] {
19
19
  if (!existsSync(dir)) return files;
@@ -37,10 +37,7 @@ export async function indexBlogs(ctx: IndexerContext): Promise<void> {
37
37
  const blogsDir = ctx.config.paths.blogs;
38
38
  const postsDir = join(blogsDir, "content", "posts");
39
39
 
40
- if (!existsSync(postsDir)) {
41
- console.log(`Blog posts directory not found: ${postsDir}`);
42
- return;
43
- }
40
+ if (!checkPath("blogs", "content/posts", postsDir)) return;
44
41
 
45
42
  if (!ctx.config.paths.blog_url) {
46
43
  console.warn(
@@ -10,15 +10,20 @@
10
10
  * Timestamp: event timestamp
11
11
  */
12
12
 
13
- import { readFileSync, existsSync } from "fs";
14
- import type { IndexerContext } from "../indexer";
13
+ import { readFileSync } from "fs";
14
+ import { checkPath, type IndexerContext } from "../indexer";
15
15
 
16
16
  export async function indexCaptures(ctx: IndexerContext): Promise<void> {
17
17
  const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
- if (!existsSync(logPath)) {
19
- console.log("No log.jsonl found, skipping captures");
18
+ if (
19
+ !checkPath(
20
+ "captures",
21
+ "log.jsonl",
22
+ logPath,
23
+ "populated by Sable session hooks",
24
+ )
25
+ )
20
26
  return;
21
- }
22
27
 
23
28
  const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
24
29
 
@@ -13,15 +13,11 @@
13
13
  import { readdirSync, existsSync } from "fs";
14
14
  import { join } from "path";
15
15
  import { spawnSync } from "child_process";
16
- import type { IndexerContext } from "../indexer";
16
+ import { checkPath, type IndexerContext } from "../indexer";
17
17
 
18
18
  export async function indexCommits(ctx: IndexerContext): Promise<void> {
19
19
  const projectsDir = ctx.config.paths.projects;
20
-
21
- if (!existsSync(projectsDir)) {
22
- console.log(`Projects directory not found: ${projectsDir}`);
23
- return;
24
- }
20
+ if (!checkPath("commits", "paths.projects", projectsDir)) return;
25
21
 
26
22
  const projects = readdirSync(projectsDir, { withFileTypes: true })
27
23
  .filter((dirent) => dirent.isDirectory())
@@ -10,15 +10,11 @@
10
10
 
11
11
  import { readdirSync, readFileSync, statSync, existsSync } from "fs";
12
12
  import { join } from "path";
13
- import type { IndexerContext } from "../indexer";
13
+ import { checkPath, type IndexerContext } from "../indexer";
14
14
 
15
15
  export async function indexDevelopment(ctx: IndexerContext): Promise<void> {
16
16
  const projectsDir = ctx.config.paths.projects;
17
-
18
- if (!existsSync(projectsDir)) {
19
- console.log(`Projects directory not found: ${projectsDir}`);
20
- return;
21
- }
17
+ if (!checkPath("development", "paths.projects", projectsDir)) return;
22
18
 
23
19
  const projects = readdirSync(projectsDir, { withFileTypes: true })
24
20
  .filter((dirent) => dirent.isDirectory())
@@ -10,16 +10,20 @@
10
10
  * Timestamp: last event timestamp per project
11
11
  */
12
12
 
13
- import { readFileSync, existsSync } from "fs";
14
- import type { IndexerContext } from "../indexer";
13
+ import { readFileSync } from "fs";
14
+ import { checkPath, type IndexerContext } from "../indexer";
15
15
 
16
16
  export async function indexEvents(ctx: IndexerContext): Promise<void> {
17
17
  const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
-
19
- if (!existsSync(logPath)) {
20
- console.log("No log.jsonl found, skipping events");
18
+ if (
19
+ !checkPath(
20
+ "events",
21
+ "log.jsonl",
22
+ logPath,
23
+ "populated by Sable session hooks",
24
+ )
25
+ )
21
26
  return;
22
- }
23
27
 
24
28
  const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
25
29
  const projectData = new Map<
@@ -10,9 +10,9 @@
10
10
  * Timestamp: file mtime as ISO 8601
11
11
  */
12
12
 
13
- import { readdirSync, readFileSync, statSync, existsSync } from "fs";
13
+ import { readdirSync, readFileSync, statSync } from "fs";
14
14
  import { join, basename, dirname } from "path";
15
- import type { IndexerContext } from "../indexer";
15
+ import { checkPath, type IndexerContext } from "../indexer";
16
16
 
17
17
  function walkMarkdownFiles(dir: string, files: string[] = []): string[] {
18
18
  const entries = readdirSync(dir, { withFileTypes: true });
@@ -33,10 +33,7 @@ function walkMarkdownFiles(dir: string, files: string[] = []): string[] {
33
33
  export async function indexExplorations(ctx: IndexerContext): Promise<void> {
34
34
  const explorationsDir = ctx.config.paths.explorations;
35
35
 
36
- if (!existsSync(explorationsDir)) {
37
- console.log(`Explorations directory not found: ${explorationsDir}`);
38
- return;
39
- }
36
+ if (!checkPath("explorations", "paths.explorations", explorationsDir)) return;
40
37
 
41
38
  const files = walkMarkdownFiles(explorationsDir);
42
39
 
@@ -14,7 +14,7 @@
14
14
 
15
15
  import { readdirSync, readFileSync, existsSync, statSync } from "fs";
16
16
  import { join, basename } from "path";
17
- import type { IndexerContext } from "../indexer";
17
+ import { checkPath, type IndexerContext } from "../indexer";
18
18
 
19
19
  export async function indexFlux(ctx: IndexerContext): Promise<void> {
20
20
  const fluxDir = ctx.config.paths.flux;
@@ -22,7 +22,7 @@ export async function indexFlux(ctx: IndexerContext): Promise<void> {
22
22
  let found = false;
23
23
 
24
24
  // Pass 1: General flux files (no project association)
25
- if (fluxDir && existsSync(fluxDir)) {
25
+ if (checkPath("flux", "paths.flux", fluxDir)) {
26
26
  found = true;
27
27
  const files = readdirSync(fluxDir).filter((f) => f.endsWith(".md"));
28
28
  for (const file of files) {
@@ -37,7 +37,7 @@ export async function indexFlux(ctx: IndexerContext): Promise<void> {
37
37
  }
38
38
 
39
39
  // Pass 2: Per-project flux files (active.md, later.md)
40
- if (fluxProjectsDir && existsSync(fluxProjectsDir)) {
40
+ if (checkPath("flux", "paths.flux_projects", fluxProjectsDir)) {
41
41
  found = true;
42
42
  const projects = readdirSync(fluxProjectsDir, { withFileTypes: true })
43
43
  .filter((d) => d.isDirectory())
@@ -57,9 +57,7 @@ export async function indexFlux(ctx: IndexerContext): Promise<void> {
57
57
  }
58
58
  }
59
59
 
60
- if (!found) {
61
- console.log("No flux directories found, skipping flux");
62
- }
60
+ if (!found) return;
63
61
  }
64
62
 
65
63
  function statusFromFilename(name: string): string {
@@ -10,15 +10,20 @@
10
10
  * Timestamp: event timestamp
11
11
  */
12
12
 
13
- import { readFileSync, existsSync } from "fs";
14
- import type { IndexerContext } from "../indexer";
13
+ import { readFileSync } from "fs";
14
+ import { checkPath, type IndexerContext } from "../indexer";
15
15
 
16
16
  export async function indexInsights(ctx: IndexerContext): Promise<void> {
17
17
  const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
- if (!existsSync(logPath)) {
19
- console.log("No log.jsonl found, skipping insights");
18
+ if (
19
+ !checkPath(
20
+ "insights",
21
+ "log.jsonl",
22
+ logPath,
23
+ "populated by Sable session hooks",
24
+ )
25
+ )
20
26
  return;
21
- }
22
27
 
23
28
  const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
24
29
 
@@ -10,16 +10,20 @@
10
10
  * Timestamp: event timestamp
11
11
  */
12
12
 
13
- import { readFileSync, existsSync } from "fs";
14
- import type { IndexerContext } from "../indexer";
13
+ import { readFileSync } from "fs";
14
+ import { checkPath, type IndexerContext } from "../indexer";
15
15
 
16
16
  export async function indexLearnings(ctx: IndexerContext): Promise<void> {
17
17
  const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
-
19
- if (!existsSync(logPath)) {
20
- console.log("No log.jsonl found, skipping learnings");
18
+ if (
19
+ !checkPath(
20
+ "learnings",
21
+ "log.jsonl",
22
+ logPath,
23
+ "populated by Sable session hooks",
24
+ )
25
+ )
21
26
  return;
22
- }
23
27
 
24
28
  const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
25
29
 
@@ -10,15 +10,20 @@
10
10
  * Timestamp: event timestamp
11
11
  */
12
12
 
13
- import { readFileSync, existsSync } from "fs";
14
- import type { IndexerContext } from "../indexer";
13
+ import { readFileSync } from "fs";
14
+ import { checkPath, type IndexerContext } from "../indexer";
15
15
 
16
16
  export async function indexObservations(ctx: IndexerContext): Promise<void> {
17
17
  const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
- if (!existsSync(logPath)) {
19
- console.log("No log.jsonl found, skipping observations");
18
+ if (
19
+ !checkPath(
20
+ "observations",
21
+ "log.jsonl",
22
+ logPath,
23
+ "populated by Sable session hooks",
24
+ )
25
+ )
20
26
  return;
21
- }
22
27
 
23
28
  const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
24
29
 
@@ -11,9 +11,9 @@
11
11
  * Timestamp: file mtime as ISO 8601
12
12
  */
13
13
 
14
- import { readdirSync, readFileSync, statSync, existsSync } from "fs";
14
+ import { readdirSync, readFileSync, statSync } from "fs";
15
15
  import { join, basename, dirname } from "path";
16
- import type { IndexerContext } from "../indexer";
16
+ import { checkPath, type IndexerContext } from "../indexer";
17
17
 
18
18
  function walkMarkdownFiles(
19
19
  dir: string,
@@ -41,10 +41,7 @@ function walkMarkdownFiles(
41
41
  export async function indexObsidian(ctx: IndexerContext): Promise<void> {
42
42
  const obsidianDir = ctx.config.paths.obsidian;
43
43
 
44
- if (!existsSync(obsidianDir)) {
45
- console.log(`Obsidian directory not found: ${obsidianDir}`);
46
- return;
47
- }
44
+ if (!checkPath("obsidian", "paths.obsidian", obsidianDir)) return;
48
45
 
49
46
  const files = walkMarkdownFiles(obsidianDir, obsidianDir);
50
47
 
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { readFileSync, statSync, existsSync } from "fs";
14
14
  import { join } from "path";
15
- import type { IndexerContext } from "../indexer";
15
+ import { checkPath, type IndexerContext } from "../indexer";
16
16
 
17
17
  function fileMtime(path: string): string {
18
18
  return statSync(path).mtime.toISOString();
@@ -27,10 +27,7 @@ function toISO(dateStr: string, fallback: string): string {
27
27
  export async function indexPersonal(ctx: IndexerContext): Promise<void> {
28
28
  const personalDir = ctx.config.paths.personal;
29
29
 
30
- if (!existsSync(personalDir)) {
31
- console.log(`Personal data directory not found: ${personalDir}`);
32
- return;
33
- }
30
+ if (!checkPath("personal", "paths.personal", personalDir)) return;
34
31
 
35
32
  // Books
36
33
  const booksPath = join(personalDir, "books.json");
@@ -10,15 +10,11 @@
10
10
 
11
11
  import { readdirSync, readFileSync, statSync, existsSync } from "fs";
12
12
  import { join } from "path";
13
- import type { IndexerContext } from "../indexer";
13
+ import { checkPath, type IndexerContext } from "../indexer";
14
14
 
15
15
  export async function indexReadmes(ctx: IndexerContext): Promise<void> {
16
16
  const projectsDir = ctx.config.paths.projects;
17
-
18
- if (!existsSync(projectsDir)) {
19
- console.log(`Projects directory not found: ${projectsDir}`);
20
- return;
21
- }
17
+ if (!checkPath("readmes", "paths.projects", projectsDir)) return;
22
18
 
23
19
  const projects = readdirSync(projectsDir, { withFileTypes: true })
24
20
  .filter((dirent) => dirent.isDirectory())
@@ -10,9 +10,9 @@
10
10
  * Timestamp: first event timestamp per session
11
11
  */
12
12
 
13
- import { readdirSync, readFileSync, existsSync } from "fs";
13
+ import { readdirSync, readFileSync } from "fs";
14
14
  import { join } from "path";
15
- import type { IndexerContext } from "../indexer";
15
+ import { checkPath, type IndexerContext } from "../indexer";
16
16
 
17
17
  interface SessionData {
18
18
  project: string;
@@ -27,17 +27,19 @@ interface SessionData {
27
27
  }
28
28
 
29
29
  export async function indexSessions(ctx: IndexerContext): Promise<void> {
30
- // Collect event files from all configured directories
31
- const eventDirs = [
32
- ctx.config.paths.session_events,
33
- ctx.config.paths.sable_events,
34
- ].filter((d): d is string => !!d && existsSync(d));
35
-
36
- if (eventDirs.length === 0) {
37
- console.log("No session events directories found, skipping sessions");
38
- return;
30
+ // Check each event directory individually for clear diagnostics
31
+ const eventDirs: string[] = [];
32
+ for (const { name, path } of [
33
+ { name: "session_events", path: ctx.config.paths.session_events },
34
+ { name: "sable_events", path: ctx.config.paths.sable_events },
35
+ ]) {
36
+ if (checkPath("sessions", name, path)) {
37
+ eventDirs.push(path);
38
+ }
39
39
  }
40
40
 
41
+ if (eventDirs.length === 0) return;
42
+
41
43
  const eventFiles: string[] = [];
42
44
  for (const dir of eventDirs) {
43
45
  const files = readdirSync(dir)
@@ -10,15 +10,20 @@
10
10
  * Timestamp: event timestamp
11
11
  */
12
12
 
13
- import { readFileSync, existsSync } from "fs";
14
- import type { IndexerContext } from "../indexer";
13
+ import { readFileSync } from "fs";
14
+ import { checkPath, type IndexerContext } from "../indexer";
15
15
 
16
16
  export async function indexTeachings(ctx: IndexerContext): Promise<void> {
17
17
  const logPath = `${ctx.config.paths.data}/log.jsonl`;
18
- if (!existsSync(logPath)) {
19
- console.log("No log.jsonl found, skipping teachings");
18
+ if (
19
+ !checkPath(
20
+ "teachings",
21
+ "log.jsonl",
22
+ logPath,
23
+ "populated by Sable session hooks",
24
+ )
25
+ )
20
26
  return;
21
- }
22
27
 
23
28
  const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
24
29
 
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.6",
3
+ "version": "1.0.8",
4
4
  "description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
5
5
  "type": "module",
6
6
  "main": "./index.ts",