ex-brain 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ex-brain",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "CLI personal knowledge base powered by seekdb",
5
5
  "module": "src/cli.ts",
6
6
  "type": "module",
@@ -34,6 +34,7 @@ import {
34
34
  subItem,
35
35
  keyValue,
36
36
  header,
37
+ separator,
37
38
  createSpinner,
38
39
  formatCount,
39
40
  type ProgressSpinner,
@@ -1483,27 +1484,106 @@ Examples:
1483
1484
 
1484
1485
  program
1485
1486
  .command("init")
1486
- .description("initialize the ebrain database")
1487
+ .description("initialize ebrain: create config, database, and show setup guide")
1487
1488
  .addHelpText(
1488
1489
  "after",
1489
1490
  `
1490
1491
  Examples:
1491
1492
  ebrain init
1493
+ ebrain init --db ./my.db
1492
1494
  `,
1493
1495
  )
1494
1496
  .action(async () => {
1495
- await withRepo(program, async () => {
1496
- const settings = await loadSettings();
1497
- const dbPath = program.opts().db ?? settings.dbPath;
1498
-
1499
- success(`Database initialized`);
1500
- keyValue("Path", dbPath);
1501
-
1502
- print(program, {
1503
- ok: true,
1504
- dbPath,
1505
- });
1497
+ const jsonOut = isJson(program);
1498
+ const settings = await loadSettings();
1499
+ const cliDb = program.opts().db;
1500
+ const dbPath = cliDb ?? settings.dbPath;
1501
+
1502
+ if (!jsonOut) {
1503
+ header("ebrain init");
1504
+ }
1505
+
1506
+ // Step 1: Create settings.json if it doesn't exist
1507
+ const { createDefaultSettings } = await import("../settings");
1508
+ const settingsCreated = await createDefaultSettings();
1509
+
1510
+ if (!jsonOut) {
1511
+ if (settingsCreated) {
1512
+ success(`Created config: ${SETTINGS_PATH}`);
1513
+ } else {
1514
+ success(`Config already exists: ${SETTINGS_PATH}`);
1515
+ }
1516
+ }
1517
+
1518
+ // Step 2: Check or initialize database
1519
+ const dbExists = await fileExists(dbPath);
1520
+ let dbInitialized = false;
1521
+
1522
+ if (dbExists) {
1523
+ // Database already exists, skip connection attempt to avoid
1524
+ // noisy errors (e.g. embedding function key mismatch)
1525
+ if (!jsonOut) {
1526
+ success(`Database already exists: ${dbPath}`);
1527
+ }
1528
+ dbInitialized = true;
1529
+ } else {
1530
+ // Try to create it without collection — embedding config may not be ready
1531
+ try {
1532
+ const db = await BrainDb.connect(dbPath, settings, { skipCollection: true });
1533
+ await db.close();
1534
+ await new Promise((r) => setTimeout(r, 200));
1535
+ dbInitialized = true;
1536
+ if (!jsonOut) {
1537
+ success(`Database initialized: ${dbPath}`);
1538
+ }
1539
+ } catch {
1540
+ if (!jsonOut) {
1541
+ warning(`Database will be auto-created on first use`);
1542
+ }
1543
+ }
1544
+ }
1545
+
1546
+ // Step 3: Show setup guide
1547
+ if (!jsonOut) {
1548
+ console.log("");
1549
+ separator();
1550
+ info("Quick Start Guide");
1551
+ console.log("");
1552
+
1553
+ subItem("1. Configure LLM (for AI queries):", 0);
1554
+ subItem(` Edit ${SETTINGS_PATH}`, 4);
1555
+ subItem(` Set llm.baseURL to your OpenAI-compatible API endpoint`, 4);
1556
+ subItem(` Set llm.apiKey or export DASHSCOPE_API_KEY`, 4);
1557
+ console.log("");
1558
+
1559
+ subItem("2. Add your first page:", 0);
1560
+ subItem(" echo '# Hello' | ebrain put hello --stdin", 4);
1561
+ console.log("");
1562
+
1563
+ subItem("3. Import a directory of markdown files:", 0);
1564
+ subItem(" ebrain import ./docs", 4);
1565
+ console.log("");
1566
+
1567
+ subItem("4. Query with AI:", 0);
1568
+ subItem(' ebrain query "What did we ship in Q4?" --llm', 4);
1569
+ console.log("");
1570
+
1571
+ subItem("5. Visualize your knowledge graph:", 0);
1572
+ subItem(" ebrain graph", 4);
1573
+ console.log("");
1574
+
1575
+ separator();
1576
+ }
1577
+
1578
+ print(program, {
1579
+ ok: true,
1580
+ settingsPath: SETTINGS_PATH,
1581
+ settingsCreated,
1582
+ dbPath,
1583
+ dbInitialized,
1506
1584
  });
1585
+
1586
+ process.exit(0);
1507
1587
  });
1508
1588
 
1509
1589
  program
package/src/db/client.ts CHANGED
@@ -107,7 +107,11 @@ export class BrainDb {
107
107
  return new Promise((resolve) => setTimeout(resolve, ms));
108
108
  }
109
109
 
110
- static async connect(dbPath: string, settings?: ResolvedSettings): Promise<BrainDb> {
110
+ static async connect(
111
+ dbPath: string,
112
+ settings?: ResolvedSettings,
113
+ options?: { skipCollection?: boolean },
114
+ ): Promise<BrainDb> {
111
115
  try {
112
116
  const client = settings?.remote
113
117
  ? await BrainDb.openRemoteClient(settings.remote)
@@ -122,6 +126,15 @@ export class BrainDb {
122
126
  await client.execute(sql);
123
127
  }
124
128
 
129
+ // Skip collection creation for init (embedding config may not be ready)
130
+ if (options?.skipCollection) {
131
+ const db = new BrainDb(dbPath, client, null as unknown as Collection);
132
+ db._isConnected = true;
133
+ db._lastConnectedAt = new Date();
134
+ console.error("\x1b[32m[DB] Connected successfully\x1b[0m");
135
+ return db;
136
+ }
137
+
125
138
  const pagesCollection = await client.getOrCreateCollection({
126
139
  name: PAGES_COLLECTION,
127
140
  embeddingFunction: createBrainEmbeddingFunction(settings?.embed),
@@ -347,8 +347,13 @@ export class BrainRepository {
347
347
  metadatas: [meta],
348
348
  });
349
349
  } catch (error) {
350
+ const msg = error instanceof Error ? error.message : String(error);
351
+ // Dimension mismatch means the collection was created with a different
352
+ // embedding model. This is non-critical — pages still work, just no search.
353
+ if (msg.includes("Dimension mismatch")) {
354
+ return;
355
+ }
350
356
  const dbError = wrapDbError(error, "syncPageToSearch", { slug });
351
- logDbError(dbError);
352
357
  // Don't throw - sync failure shouldn't break the main flow
353
358
  console.warn(`[BrainRepo] syncPageToSearch failed for ${slug}: ${dbError.message}`);
354
359
  }
@@ -384,8 +389,11 @@ export class BrainRepository {
384
389
  metadatas: metas,
385
390
  });
386
391
  } catch (error) {
392
+ const msg = error instanceof Error ? error.message : String(error);
393
+ if (msg.includes("Dimension mismatch")) {
394
+ return;
395
+ }
387
396
  const dbError = wrapDbError(error, "syncPagesToSearch", { count: slugs.length });
388
- logDbError(dbError);
389
397
  // Don't throw - sync failure shouldn't break the main flow
390
398
  console.warn(`[BrainRepo] syncPagesToSearch failed: ${dbError.message}`);
391
399
  }
package/src/settings.ts CHANGED
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { fileExists, readTextFile } from "./markdown/io";
5
5
 
6
- const SETTINGS_DIR = join(homedir(), ".ebrain");
6
+ export const SETTINGS_DIR = join(homedir(), ".ebrain");
7
7
  export const SETTINGS_PATH = join(SETTINGS_DIR, "settings.json");
8
8
  export const DEFAULT_DB_PATH = resolve(SETTINGS_DIR, "data", "ebrain.db");
9
9
 
@@ -150,6 +150,54 @@ export async function readSettingsFile(): Promise<unknown | null> {
150
150
  }
151
151
  }
152
152
 
153
+ /**
154
+ * Generate a minimal settings.json if it doesn't already exist.
155
+ * Returns true if a new file was created.
156
+ */
157
+ export async function createDefaultSettings(): Promise<boolean> {
158
+ if (await fileExists(SETTINGS_PATH)) {
159
+ return false;
160
+ }
161
+
162
+ const { mkdirSync, writeFileSync } = await import("node:fs");
163
+ mkdirSync(SETTINGS_DIR, { recursive: true });
164
+
165
+ // All fields present but empty — user fills in their values
166
+ const defaults = {
167
+ db: {
168
+ path: "",
169
+ remote: {
170
+ host: "",
171
+ port: 0,
172
+ user: "",
173
+ password: "",
174
+ database: "",
175
+ tenant: "",
176
+ },
177
+ },
178
+ embed: {
179
+ provider: "hash",
180
+ baseURL: "",
181
+ model: "",
182
+ dimensions: 0,
183
+ apiKey: "",
184
+ apiKeyEnv: "",
185
+ },
186
+ llm: {
187
+ baseURL: "",
188
+ model: "",
189
+ apiKey: "",
190
+ apiKeyEnv: "",
191
+ },
192
+ extraction: {
193
+ confidenceThreshold: 0.7,
194
+ },
195
+ };
196
+
197
+ writeFileSync(SETTINGS_PATH, JSON.stringify(defaults, null, 2) + "\n", "utf-8");
198
+ return true;
199
+ }
200
+
153
201
  export function resolveSettings(parsed: z.infer<typeof SettingsSchema>): ResolvedSettings {
154
202
  const dbConf = parsed.db ?? {};
155
203
  const remoteConf = dbConf.remote ?? {};
@@ -215,7 +263,8 @@ function resolveExtraction(conf: { confidenceThreshold?: number }): ResolvedExtr
215
263
  // ---------------------------------------------------------------------------
216
264
 
217
265
  function nonEmpty(val: string | undefined, fallback: string): string {
218
- return val?.trim() ?? fallback;
266
+ const trimmed = val?.trim();
267
+ return trimmed || fallback;
219
268
  }
220
269
 
221
270
  function numOr(val: number | string | undefined, fallback: number): number {