ex-brain 0.1.1 → 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.
- package/README.md +48 -0
- package/package.json +2 -1
- package/src/ai/compiler.ts +18 -53
- package/src/ai/entity-link.ts +31 -62
- package/src/ai/llm-client.ts +291 -0
- package/src/ai/timeline-extractor.ts +29 -62
- package/src/commands/index.ts +612 -86
- package/src/db/client.ts +121 -15
- package/src/db/errors.ts +178 -0
- package/src/db/schema.ts +1 -0
- package/src/mcp/server.ts +400 -237
- package/src/repositories/brain-repo.ts +576 -358
- package/src/settings.ts +23 -2
- package/src/types/index.ts +1 -0
- package/src/utils/cli-output.ts +569 -0
- package/src/utils/query-sanitizer.ts +63 -0
package/src/db/client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { ResolvedSettings } from "../settings";
|
|
|
6
6
|
import { createBrainEmbeddingFunction } from "../ai/embed-factory";
|
|
7
7
|
import { DEFAULT_DB_NAME, PAGES_COLLECTION } from "../config";
|
|
8
8
|
import { SQL_SCHEMA } from "./schema";
|
|
9
|
+
import { DbError, wrapDbError, DbErrorCategory } from "./errors";
|
|
9
10
|
|
|
10
11
|
function useRemoteSeekdb(): boolean {
|
|
11
12
|
return Boolean(process.env.EBRAIN_SEEKDB_HOST?.trim());
|
|
@@ -15,30 +16,127 @@ function seekdbPassword(): string {
|
|
|
15
16
|
return process.env.EBRAIN_SEEKDB_PASSWORD ?? process.env.SEEKDB_PASSWORD ?? "";
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const MAX_RETRY_ATTEMPTS = 3;
|
|
20
|
+
const RETRY_DELAY_MS = 1000;
|
|
21
|
+
const RETRY_BACKOFF_FACTOR = 2;
|
|
22
|
+
|
|
18
23
|
export class BrainDb {
|
|
24
|
+
private _isConnected = false;
|
|
25
|
+
private _lastConnectedAt: Date | null = null;
|
|
26
|
+
|
|
19
27
|
private constructor(
|
|
20
28
|
public readonly dbPath: string,
|
|
21
29
|
public readonly client: SeekdbClient,
|
|
22
30
|
public readonly pagesCollection: Collection,
|
|
23
31
|
) {}
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Check if the database is currently connected.
|
|
35
|
+
*/
|
|
36
|
+
get isConnected(): boolean {
|
|
37
|
+
return this._isConnected;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the last successful connection timestamp.
|
|
42
|
+
*/
|
|
43
|
+
get lastConnectedAt(): Date | null {
|
|
44
|
+
return this._lastConnectedAt;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Execute with automatic reconnection on failure.
|
|
49
|
+
*/
|
|
50
|
+
async executeWithRetry<T>(
|
|
51
|
+
operation: () => Promise<T>,
|
|
52
|
+
operationName: string,
|
|
53
|
+
): Promise<T> {
|
|
54
|
+
let lastError: DbError | null = null;
|
|
55
|
+
|
|
56
|
+
for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
|
|
57
|
+
try {
|
|
58
|
+
const result = await operation();
|
|
59
|
+
if (!this._isConnected) {
|
|
60
|
+
this._isConnected = true;
|
|
61
|
+
this._lastConnectedAt = new Date();
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const wrappedError = wrapDbError(error, operationName as any);
|
|
66
|
+
lastError = wrappedError;
|
|
67
|
+
|
|
68
|
+
// Only retry on connection/timeout errors
|
|
69
|
+
if (!wrappedError.isRetryable() || attempt === MAX_RETRY_ATTEMPTS) {
|
|
70
|
+
throw wrappedError;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Check if it's a connection error that might be resolved by reconnecting
|
|
74
|
+
if (wrappedError.category === DbErrorCategory.CONNECTION) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`\x1b[33m[DB]\x1b[0m Connection error on attempt ${attempt}/${MAX_RETRY_ATTEMPTS}, retrying...`,
|
|
77
|
+
);
|
|
78
|
+
await this.attemptReconnect();
|
|
79
|
+
} else {
|
|
80
|
+
// Exponential backoff for other retryable errors
|
|
81
|
+
const delay = RETRY_DELAY_MS * Math.pow(RETRY_BACKOFF_FACTOR, attempt - 1);
|
|
82
|
+
console.warn(
|
|
83
|
+
`\x1b[33m[DB]\x1b[0m ${operationName} failed on attempt ${attempt}/${MAX_RETRY_ATTEMPTS}, retrying in ${delay}ms...`,
|
|
84
|
+
);
|
|
85
|
+
await this.sleep(delay);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
34
88
|
}
|
|
35
89
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
90
|
+
throw lastError;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async attemptReconnect(): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
// Test if we can execute a simple query
|
|
96
|
+
await this.client.execute("SELECT 1");
|
|
97
|
+
this._isConnected = true;
|
|
98
|
+
this._lastConnectedAt = new Date();
|
|
99
|
+
console.error("\x1b[32m[DB] Reconnected successfully\x1b[0m");
|
|
100
|
+
} catch {
|
|
101
|
+
// Connection still failed, will retry on next operation
|
|
102
|
+
this._isConnected = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private sleep(ms: number): Promise<void> {
|
|
107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
static async connect(dbPath: string, settings?: ResolvedSettings): Promise<BrainDb> {
|
|
111
|
+
try {
|
|
112
|
+
const client = settings?.remote
|
|
113
|
+
? await BrainDb.openRemoteClient(settings.remote)
|
|
114
|
+
: useRemoteSeekdb()
|
|
115
|
+
? await BrainDb.openRemoteClientFromEnv()
|
|
116
|
+
: await BrainDb.openEmbeddedClient(dbPath);
|
|
117
|
+
|
|
118
|
+
// Test connection with a simple query
|
|
119
|
+
await client.execute("SELECT 1");
|
|
40
120
|
|
|
41
|
-
|
|
121
|
+
for (const sql of SQL_SCHEMA) {
|
|
122
|
+
await client.execute(sql);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const pagesCollection = await client.getOrCreateCollection({
|
|
126
|
+
name: PAGES_COLLECTION,
|
|
127
|
+
embeddingFunction: createBrainEmbeddingFunction(settings?.embed),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const db = new BrainDb(dbPath, client, pagesCollection);
|
|
131
|
+
db._isConnected = true;
|
|
132
|
+
db._lastConnectedAt = new Date();
|
|
133
|
+
console.error("\x1b[32m[DB] Connected successfully\x1b[0m");
|
|
134
|
+
return db;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const wrappedError = wrapDbError(error, "connect");
|
|
137
|
+
console.error(`\x1b[31m[DB]\x1b[0m Connection failed:`, wrappedError.toJSON());
|
|
138
|
+
throw wrappedError;
|
|
139
|
+
}
|
|
42
140
|
}
|
|
43
141
|
|
|
44
142
|
private static async openEmbeddedClient(dbPath: string): Promise<SeekdbClient> {
|
|
@@ -96,6 +194,14 @@ export class BrainDb {
|
|
|
96
194
|
}
|
|
97
195
|
|
|
98
196
|
async close(): Promise<void> {
|
|
99
|
-
|
|
197
|
+
try {
|
|
198
|
+
await this.client.close();
|
|
199
|
+
this._isConnected = false;
|
|
200
|
+
console.error("\x1b[32m[DB] Disconnected\x1b[0m");
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const wrappedError = wrapDbError(error, "close");
|
|
203
|
+
console.error(`\x1b[31m[DB]\x1b[0m Error closing connection:`, wrappedError.message);
|
|
204
|
+
// Don't throw on close errors - best effort
|
|
205
|
+
}
|
|
100
206
|
}
|
|
101
207
|
}
|
package/src/db/errors.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database error types and utilities.
|
|
3
|
+
* Provides unified error handling for all database operations.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Database error categories
|
|
8
|
+
*/
|
|
9
|
+
export const DbErrorCategory = {
|
|
10
|
+
CONNECTION: "CONNECTION",
|
|
11
|
+
QUERY: "QUERY",
|
|
12
|
+
SCHEMA: "SCHEMA",
|
|
13
|
+
VALIDATION: "VALIDATION",
|
|
14
|
+
NOT_FOUND: "NOT_FOUND",
|
|
15
|
+
CONSTRAINT: "CONSTRAINT",
|
|
16
|
+
TIMEOUT: "TIMEOUT",
|
|
17
|
+
UNKNOWN: "UNKNOWN",
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export type DbErrorCategory = (typeof DbErrorCategory)[keyof typeof DbErrorCategory];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Database operation types for error context
|
|
24
|
+
*/
|
|
25
|
+
export type DbOperation =
|
|
26
|
+
| "getPage"
|
|
27
|
+
| "putPage"
|
|
28
|
+
| "listPages"
|
|
29
|
+
| "deletePage"
|
|
30
|
+
| "search"
|
|
31
|
+
| "query"
|
|
32
|
+
| "syncPageToSearch"
|
|
33
|
+
| "syncPagesToSearch"
|
|
34
|
+
| "embedAll"
|
|
35
|
+
| "link"
|
|
36
|
+
| "unlink"
|
|
37
|
+
| "timeline"
|
|
38
|
+
| "timelineAdd"
|
|
39
|
+
| "timelineAddBatch"
|
|
40
|
+
| "timelineDelete"
|
|
41
|
+
| "timelineUpdate"
|
|
42
|
+
| "timelineGlobal"
|
|
43
|
+
| "tags"
|
|
44
|
+
| "tag"
|
|
45
|
+
| "untag"
|
|
46
|
+
| "readRaw"
|
|
47
|
+
| "writeRaw"
|
|
48
|
+
| "backlinks"
|
|
49
|
+
| "allSlugs"
|
|
50
|
+
| "stats"
|
|
51
|
+
| "findSimilarSlug"
|
|
52
|
+
| "ensureEntityPage"
|
|
53
|
+
| "compilePage"
|
|
54
|
+
| "extractAndAddTimeline"
|
|
55
|
+
| "ingestContent"
|
|
56
|
+
| "init"
|
|
57
|
+
| "connect"
|
|
58
|
+
| "close";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Unified database error class
|
|
62
|
+
*/
|
|
63
|
+
export class DbError extends Error {
|
|
64
|
+
constructor(
|
|
65
|
+
message: string,
|
|
66
|
+
public readonly category: DbErrorCategory,
|
|
67
|
+
public readonly operation: DbOperation,
|
|
68
|
+
public readonly dbCause?: unknown,
|
|
69
|
+
public readonly retryable: boolean = false,
|
|
70
|
+
) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = "DbError";
|
|
73
|
+
// Maintains proper stack trace in V8 (Node.js)
|
|
74
|
+
if (Error.captureStackTrace) {
|
|
75
|
+
Error.captureStackTrace(this, DbError);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if this error is retryable
|
|
81
|
+
*/
|
|
82
|
+
isRetryable(): boolean {
|
|
83
|
+
return this.retryable;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert to JSON for logging
|
|
88
|
+
*/
|
|
89
|
+
toJSON(): Record<string, unknown> {
|
|
90
|
+
return {
|
|
91
|
+
name: this.name,
|
|
92
|
+
message: this.message,
|
|
93
|
+
category: this.category,
|
|
94
|
+
operation: this.operation,
|
|
95
|
+
retryable: this.retryable,
|
|
96
|
+
dbCause: this.dbCause instanceof Error
|
|
97
|
+
? { name: this.dbCause.name, message: this.dbCause.message }
|
|
98
|
+
: this.dbCause,
|
|
99
|
+
stack: this.stack,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Create database error from any caught exception
|
|
106
|
+
*/
|
|
107
|
+
export function wrapDbError(
|
|
108
|
+
error: unknown,
|
|
109
|
+
operation: DbOperation,
|
|
110
|
+
context?: Record<string, unknown>,
|
|
111
|
+
): DbError {
|
|
112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
113
|
+
const cause = error instanceof Error ? error : undefined;
|
|
114
|
+
|
|
115
|
+
// Determine error category and retryability based on error message/type
|
|
116
|
+
let category: DbErrorCategory = "UNKNOWN";
|
|
117
|
+
let retryable = false;
|
|
118
|
+
|
|
119
|
+
const errorStr = message.toLowerCase();
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
errorStr.includes("connect") ||
|
|
123
|
+
errorStr.includes("connection") ||
|
|
124
|
+
errorStr.includes("econnrefused") ||
|
|
125
|
+
errorStr.includes("etimedout") ||
|
|
126
|
+
errorStr.includes("network")
|
|
127
|
+
) {
|
|
128
|
+
category = "CONNECTION";
|
|
129
|
+
retryable = true;
|
|
130
|
+
} else if (
|
|
131
|
+
errorStr.includes("timeout") ||
|
|
132
|
+
errorStr.includes("timed out")
|
|
133
|
+
) {
|
|
134
|
+
category = "TIMEOUT";
|
|
135
|
+
retryable = true;
|
|
136
|
+
} else if (
|
|
137
|
+
errorStr.includes("not found") ||
|
|
138
|
+
errorStr.includes("no such table") ||
|
|
139
|
+
errorStr.includes("no such database")
|
|
140
|
+
) {
|
|
141
|
+
category = "NOT_FOUND";
|
|
142
|
+
} else if (
|
|
143
|
+
errorStr.includes("constraint") ||
|
|
144
|
+
errorStr.includes("duplicate") ||
|
|
145
|
+
errorStr.includes("unique")
|
|
146
|
+
) {
|
|
147
|
+
category = "CONSTRAINT";
|
|
148
|
+
} else if (
|
|
149
|
+
errorStr.includes("syntax") ||
|
|
150
|
+
errorStr.includes("parse") ||
|
|
151
|
+
errorStr.includes("invalid")
|
|
152
|
+
) {
|
|
153
|
+
category = "VALIDATION";
|
|
154
|
+
} else if (
|
|
155
|
+
errorStr.includes("schema") ||
|
|
156
|
+
errorStr.includes("column")
|
|
157
|
+
) {
|
|
158
|
+
category = "SCHEMA";
|
|
159
|
+
} else {
|
|
160
|
+
category = "QUERY";
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const fullMessage = context
|
|
164
|
+
? `${operation} failed: ${message} ${JSON.stringify(context)}`
|
|
165
|
+
: `${operation} failed: ${message}`;
|
|
166
|
+
|
|
167
|
+
return new DbError(fullMessage, category, operation, cause, retryable);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Log database error with context
|
|
172
|
+
*/
|
|
173
|
+
export function logDbError(
|
|
174
|
+
error: DbError,
|
|
175
|
+
logger: { error: (msg: string, meta?: Record<string, unknown>) => void } = console,
|
|
176
|
+
): void {
|
|
177
|
+
logger.error("[DB Error]", error.toJSON());
|
|
178
|
+
}
|
package/src/db/schema.ts
CHANGED