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 +1 -1
- package/src/commands/index.ts +92 -12
- package/src/db/client.ts +14 -1
- package/src/repositories/brain-repo.ts +10 -2
- package/src/settings.ts +51 -2
package/package.json
CHANGED
package/src/commands/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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(
|
|
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
|
-
|
|
266
|
+
const trimmed = val?.trim();
|
|
267
|
+
return trimmed || fallback;
|
|
219
268
|
}
|
|
220
269
|
|
|
221
270
|
function numOr(val: number | string | undefined, fallback: number): number {
|