ex-brain 0.1.0 → 0.2.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.
@@ -0,0 +1,235 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ import { z } from "zod";
4
+ import { fileExists, readTextFile } from "./markdown/io";
5
+
6
+ const SETTINGS_DIR = join(homedir(), ".ebrain");
7
+ export const SETTINGS_PATH = join(SETTINGS_DIR, "settings.json");
8
+ export const DEFAULT_DB_PATH = resolve(SETTINGS_DIR, "data", "ebrain.db");
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Schema
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const RemoteDbSchema = z.object({
15
+ host: z.string().optional(),
16
+ port: z.number().optional(),
17
+ user: z.string().optional(),
18
+ password: z.string().optional(),
19
+ database: z.string().optional(),
20
+ tenant: z.string().optional(),
21
+ });
22
+
23
+ const EmbedSchema = z.object({
24
+ provider: z.enum(["hash", "openai_compatible"]).optional(),
25
+ baseURL: z.string().optional(),
26
+ model: z.string().optional(),
27
+ dimensions: z.number().optional(),
28
+ apiKey: z.string().optional(),
29
+ apiKeyEnv: z.string().optional(),
30
+ });
31
+
32
+ const LLMSchema = z.object({
33
+ baseURL: z.string().optional(),
34
+ model: z.string().optional(),
35
+ apiKey: z.string().optional(),
36
+ apiKeyEnv: z.string().optional(),
37
+ });
38
+
39
+ const SettingsSchema = z.object({
40
+ db: z
41
+ .object({
42
+ path: z.string().optional(),
43
+ remote: RemoteDbSchema.optional(),
44
+ })
45
+ .optional(),
46
+ embed: EmbedSchema.optional(),
47
+ llm: LLMSchema.optional(),
48
+ extraction: z
49
+ .object({
50
+ confidenceThreshold: z.number().min(0).max(1).optional(),
51
+ })
52
+ .optional(),
53
+ });
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Resolved types (all values present after defaults + env merge)
57
+ // ---------------------------------------------------------------------------
58
+
59
+ export interface ResolvedSettings {
60
+ dbPath: string;
61
+ remote: ResolvedRemoteDb | null;
62
+ embed: ResolvedEmbed;
63
+ llm: ResolvedLLM;
64
+ extraction: ResolvedExtraction;
65
+ }
66
+
67
+ export interface ResolvedExtraction {
68
+ confidenceThreshold: number;
69
+ }
70
+
71
+ export interface ResolvedRemoteDb {
72
+ host: string;
73
+ port: number;
74
+ user: string;
75
+ password: string;
76
+ database: string;
77
+ tenant: string;
78
+ }
79
+
80
+ export interface ResolvedEmbed {
81
+ provider: "hash" | "openai_compatible";
82
+ baseURL: string;
83
+ model: string;
84
+ dimensions: number;
85
+ apiKey: string;
86
+ apiKeyEnv: string;
87
+ }
88
+
89
+ export interface ResolvedLLM {
90
+ baseURL: string;
91
+ model: string;
92
+ apiKey: string;
93
+ apiKeyEnv: string;
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // Defaults
98
+ // ---------------------------------------------------------------------------
99
+
100
+ const DEFAULT_REMOTE = {
101
+ port: 3306,
102
+ user: "root",
103
+ password: "",
104
+ database: "ebrain",
105
+ tenant: "",
106
+ };
107
+
108
+ const DEFAULT_EMBED = {
109
+ provider: "hash" as const,
110
+ baseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
111
+ model: "text-embedding-v4",
112
+ dimensions: 1024,
113
+ apiKey: "",
114
+ apiKeyEnv: "DASHSCOPE_API_KEY",
115
+ };
116
+
117
+ const DEFAULT_LLM = {
118
+ baseURL: "",
119
+ model: "qwen-plus",
120
+ apiKey: "",
121
+ apiKeyEnv: "DASHSCOPE_API_KEY",
122
+ };
123
+
124
+ const DEFAULT_EXTRACTION = {
125
+ confidenceThreshold: 0.7,
126
+ };
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Load & resolve
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export async function loadSettings(): Promise<ResolvedSettings> {
133
+ const raw = await readSettingsFile();
134
+ const parsed = SettingsSchema.parse(raw ?? {});
135
+ return resolveSettings(parsed);
136
+ }
137
+
138
+ export async function readSettingsFile(): Promise<unknown | null> {
139
+ if (!(await fileExists(SETTINGS_PATH))) {
140
+ return null;
141
+ }
142
+ const text = await readTextFile(SETTINGS_PATH);
143
+ try {
144
+ return JSON.parse(text) as unknown;
145
+ } catch {
146
+ console.warn(
147
+ `[ebrain] Failed to parse ${SETTINGS_PATH}, using defaults.`,
148
+ );
149
+ return null;
150
+ }
151
+ }
152
+
153
+ export function resolveSettings(parsed: z.infer<typeof SettingsSchema>): ResolvedSettings {
154
+ const dbConf = parsed.db ?? {};
155
+ const remoteConf = dbConf.remote ?? {};
156
+ const embedConf = parsed.embed ?? {};
157
+ const extractionConf = parsed.extraction ?? {};
158
+
159
+ // Remote: settings → env → defaults
160
+ const host = remoteConf.host ?? process.env.EBRAIN_SEEKDB_HOST ?? "";
161
+ if (host) {
162
+ const remote: ResolvedRemoteDb = {
163
+ host: host.trim(),
164
+ port: numOr(remoteConf.port ?? process.env.EBRAIN_SEEKDB_PORT, DEFAULT_REMOTE.port),
165
+ user: nonEmpty(remoteConf.user ?? process.env.EBRAIN_SEEKDB_USER, DEFAULT_REMOTE.user),
166
+ password: nonEmpty(
167
+ remoteConf.password ?? process.env.EBRAIN_SEEKDB_PASSWORD,
168
+ DEFAULT_REMOTE.password,
169
+ ),
170
+ database: nonEmpty(
171
+ remoteConf.database ?? process.env.EBRAIN_SEEKDB_DATABASE,
172
+ DEFAULT_REMOTE.database,
173
+ ),
174
+ tenant: nonEmpty(remoteConf.tenant ?? process.env.EBRAIN_SEEKDB_TENANT, ""),
175
+ };
176
+ return { dbPath: dbConf.path ?? DEFAULT_DB_PATH, remote, embed: resolveEmbed(embedConf), llm: resolveLLM(parsed.llm ?? {}), extraction: resolveExtraction(extractionConf) };
177
+ }
178
+
179
+ // Local mode
180
+ const dbPath = dbConf.path
181
+ ? resolvePath(dbConf.path)
182
+ : DEFAULT_DB_PATH;
183
+ return { dbPath, remote: null, embed: resolveEmbed(embedConf), llm: resolveLLM(parsed.llm ?? {}), extraction: resolveExtraction(extractionConf) };
184
+ }
185
+
186
+ function resolveEmbed(conf: z.infer<typeof EmbedSchema>): ResolvedEmbed {
187
+ const provider = nonEmpty(
188
+ conf.provider ?? process.env.EBRAIN_EMBED_PROVIDER,
189
+ DEFAULT_EMBED.provider,
190
+ ).trim().toLowerCase() as "hash" | "openai_compatible";
191
+ const baseURL = nonEmpty(conf.baseURL ?? process.env.EBRAIN_EMBED_BASE_URL, DEFAULT_EMBED.baseURL);
192
+ const model = nonEmpty(conf.model ?? process.env.EBRAIN_EMBED_MODEL, DEFAULT_EMBED.model);
193
+ const dimensions = numOr(conf.dimensions ?? process.env.EBRAIN_EMBED_DIMENSIONS, DEFAULT_EMBED.dimensions);
194
+ const apiKey = nonEmpty(conf.apiKey ?? process.env.EBRAIN_EMBED_API_KEY, "");
195
+ const apiKeyEnv = nonEmpty(conf.apiKeyEnv ?? process.env.EBRAIN_EMBED_API_KEY_ENV, DEFAULT_EMBED.apiKeyEnv);
196
+ return { provider, baseURL, model, dimensions, apiKey, apiKeyEnv };
197
+ }
198
+
199
+ function resolveLLM(conf: z.infer<typeof LLMSchema>): ResolvedLLM {
200
+ const baseURL = nonEmpty(conf.baseURL, DEFAULT_LLM.baseURL);
201
+ const model = nonEmpty(conf.model, DEFAULT_LLM.model);
202
+ const apiKey = nonEmpty(conf.apiKey, DEFAULT_LLM.apiKey);
203
+ const apiKeyEnv = nonEmpty(conf.apiKeyEnv, DEFAULT_LLM.apiKeyEnv);
204
+ return { baseURL, model, apiKey, apiKeyEnv };
205
+ }
206
+
207
+ function resolveExtraction(conf: { confidenceThreshold?: number }): ResolvedExtraction {
208
+ const threshold = conf.confidenceThreshold ?? process.env.EBRAIN_CONFIDENCE_THRESHOLD;
209
+ const value = typeof threshold === "number" ? threshold : (threshold ? parseFloat(threshold) : DEFAULT_EXTRACTION.confidenceThreshold);
210
+ return { confidenceThreshold: Math.max(0, Math.min(1, value)) };
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Helpers
215
+ // ---------------------------------------------------------------------------
216
+
217
+ function nonEmpty(val: string | undefined, fallback: string): string {
218
+ return val?.trim() ?? fallback;
219
+ }
220
+
221
+ function numOr(val: number | string | undefined, fallback: number): number {
222
+ if (typeof val === "number") return val;
223
+ if (typeof val === "string") {
224
+ const n = Number(val.trim());
225
+ if (Number.isFinite(n)) return n;
226
+ }
227
+ return fallback;
228
+ }
229
+
230
+ function resolvePath(p: string): string {
231
+ if (p.startsWith("~")) {
232
+ return join(homedir(), p.slice(1));
233
+ }
234
+ return resolve(p);
235
+ }
@@ -0,0 +1,56 @@
1
+ export type PageType =
2
+ | "person"
3
+ | "company"
4
+ | "deal"
5
+ | "yc"
6
+ | "civic"
7
+ | "project"
8
+ | "note"
9
+ | "other";
10
+
11
+ export interface PageRecord {
12
+ slug: string;
13
+ type: PageType | string;
14
+ title: string;
15
+ compiledTruth: string;
16
+ timeline: string;
17
+ frontmatter: Record<string, unknown>;
18
+ createdAt: string;
19
+ updatedAt: string;
20
+ }
21
+
22
+ export interface TimelineEntry {
23
+ id?: number;
24
+ pageSlug: string;
25
+ date: string;
26
+ source: string;
27
+ summary: string;
28
+ detail: string;
29
+ importance?: number;
30
+ }
31
+
32
+ export interface SearchHit {
33
+ slug: string;
34
+ title: string;
35
+ type: string;
36
+ score: number;
37
+ excerpt?: string;
38
+ updatedAt?: string;
39
+ }
40
+
41
+ export interface BrainStats {
42
+ pages: number;
43
+ links: number;
44
+ tags: number;
45
+ timelineEntries: number;
46
+ rawRows: number;
47
+ }
48
+
49
+ export interface PutPageInput {
50
+ slug: string;
51
+ type: string;
52
+ title: string;
53
+ compiledTruth: string;
54
+ timeline?: string;
55
+ frontmatter?: Record<string, unknown>;
56
+ }