@voidwire/lore 1.0.5 → 1.0.7

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
@@ -7,8 +7,8 @@
7
7
  * Usage:
8
8
  * import { getConfig } from "./config";
9
9
  * const config = getConfig();
10
- * console.log(config.paths.data); // /Users/rudy/.local/share/lore
11
- * console.log(config.database.sqlite); // /Users/rudy/.local/share/lore/lore.db
10
+ * console.log(config.paths.data); // ~/.local/share/lore
11
+ * console.log(config.database.sqlite); // ~/.local/share/lore/lore.db
12
12
  */
13
13
 
14
14
  import { readFileSync } from "fs";
@@ -27,9 +27,11 @@ export interface LoreConfig {
27
27
  sable_events?: string;
28
28
  flux?: string;
29
29
  flux_projects?: string;
30
+ blog_url?: string;
30
31
  };
31
32
  database: {
32
33
  sqlite: string;
34
+ custom_sqlite?: string;
33
35
  };
34
36
  }
35
37
 
@@ -129,9 +131,14 @@ export function getConfig(): LoreConfig {
129
131
  typeof paths.flux_projects === "string"
130
132
  ? resolvePath(paths.flux_projects)
131
133
  : undefined,
134
+ blog_url: typeof paths.blog_url === "string" ? paths.blog_url : undefined,
132
135
  },
133
136
  database: {
134
137
  sqlite: resolvePath(database.sqlite as string),
138
+ custom_sqlite:
139
+ typeof database.custom_sqlite === "string"
140
+ ? resolvePath(database.custom_sqlite)
141
+ : undefined,
135
142
  },
136
143
  };
137
144
 
package/lib/db.ts CHANGED
@@ -9,11 +9,22 @@ import { Database } from "bun:sqlite";
9
9
  import { existsSync } from "fs";
10
10
  import { getConfig } from "./config";
11
11
 
12
- // Use Homebrew SQLite on macOS to enable extension loading
12
+ // Load custom SQLite from config to enable extension loading
13
13
  // Must be called before any Database instances are created
14
- const HOMEBREW_SQLITE = "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib";
15
- if (existsSync(HOMEBREW_SQLITE)) {
16
- Database.setCustomSQLite(HOMEBREW_SQLITE);
14
+ const config = getConfig();
15
+ if (config.database.custom_sqlite) {
16
+ if (!existsSync(config.database.custom_sqlite)) {
17
+ throw new Error(
18
+ `database.custom_sqlite path does not exist: ${config.database.custom_sqlite}`,
19
+ );
20
+ }
21
+ Database.setCustomSQLite(config.database.custom_sqlite);
22
+ } else {
23
+ throw new Error(
24
+ "database.custom_sqlite not set in ~/.config/lore/config.toml.\n" +
25
+ "Required for sqlite-vec extension loading.\n" +
26
+ 'macOS: custom_sqlite = "/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib"',
27
+ );
17
28
  }
18
29
 
19
30
  /**
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,9 +37,12 @@ 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;
40
+ if (!checkPath("blogs", "content/posts", postsDir)) return;
41
+
42
+ if (!ctx.config.paths.blog_url) {
43
+ console.warn(
44
+ "WARNING: paths.blog_url not set in config.toml — blog post URLs will not be generated",
45
+ );
43
46
  }
44
47
 
45
48
  const files = walkMarkdownFiles(postsDir);
@@ -117,9 +120,10 @@ export async function indexBlogs(ctx: IndexerContext): Promise<void> {
117
120
  // Topic from categories
118
121
  const topic = categories.length > 0 ? categories.join(" ") : "";
119
122
 
120
- // URL from slug or filename
123
+ // URL from slug or filename — requires blog_url in config
124
+ const blogUrl = ctx.config.paths.blog_url;
121
125
  const urlSlug = slug || basename(filePath, ".md");
122
- const url = `https://labs.voidwire.info/posts/${urlSlug}/`;
126
+ const url = blogUrl ? `${blogUrl}/posts/${urlSlug}/` : "";
123
127
 
124
128
  // Word count
125
129
  const wordCount = content.split(/\s+/).filter(Boolean).length;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voidwire/lore",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Unified knowledge CLI - Search, list, and capture your indexed knowledge",
5
5
  "type": "module",
6
6
  "main": "./index.ts",