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.
- package/README.md +87 -37
- package/package.json +6 -5
- package/src/ai/compiler.ts +494 -0
- package/src/ai/embed-factory.ts +116 -0
- package/src/ai/entity-link.ts +195 -0
- package/src/ai/hash-embed.ts +30 -0
- package/src/ai/llm-client.ts +291 -0
- package/src/ai/timeline-extractor.ts +403 -0
- package/src/cli.ts +16 -0
- package/src/commands/compile-cmd.ts +208 -0
- package/src/commands/graph-cmd.ts +1070 -0
- package/src/commands/index.ts +1973 -0
- package/src/config.ts +80 -0
- package/src/db/client.ts +207 -0
- package/src/db/errors.ts +178 -0
- package/src/db/schema.ts +50 -0
- package/src/markdown/io.ts +61 -0
- package/src/markdown/parser.ts +72 -0
- package/src/mcp/server.ts +703 -0
- package/src/repositories/brain-repo.ts +990 -0
- package/src/settings.ts +235 -0
- package/src/types/index.ts +56 -0
- package/src/utils/cli-output.ts +569 -0
- package/src/utils/progress.ts +171 -0
- package/src/utils/query-sanitizer.ts +63 -0
- package/dist/cli.js +0 -93543
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress indicator with spinner animation for long-running operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ProgressIndicator {
|
|
6
|
+
start(message: string): void;
|
|
7
|
+
update(message: string): void;
|
|
8
|
+
succeed(message?: string): void;
|
|
9
|
+
fail(message?: string): void;
|
|
10
|
+
stop(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
14
|
+
const SPINNER_INTERVAL = 80;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a progress indicator with spinner animation.
|
|
18
|
+
* @param stream Output stream (default: process.stderr)
|
|
19
|
+
*/
|
|
20
|
+
export function createProgress(stream: NodeJS.WritableStream = process.stderr): ProgressIndicator {
|
|
21
|
+
let frameIndex = 0;
|
|
22
|
+
let interval: Timer | null = null;
|
|
23
|
+
let currentMessage = '';
|
|
24
|
+
let isRunning = false;
|
|
25
|
+
|
|
26
|
+
function clearLine() {
|
|
27
|
+
stream.write('\r\x1b[K');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function render() {
|
|
31
|
+
if (!isRunning) return;
|
|
32
|
+
const frame = SPINNER_FRAMES[frameIndex];
|
|
33
|
+
clearLine();
|
|
34
|
+
stream.write(`\x1b[36m${frame}\x1b[0m ${currentMessage}`);
|
|
35
|
+
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function start(message: string) {
|
|
39
|
+
if (isRunning) stop();
|
|
40
|
+
currentMessage = message;
|
|
41
|
+
isRunning = true;
|
|
42
|
+
frameIndex = 0;
|
|
43
|
+
render();
|
|
44
|
+
interval = setInterval(render, SPINNER_INTERVAL);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function update(message: string) {
|
|
48
|
+
currentMessage = message;
|
|
49
|
+
if (!isRunning) {
|
|
50
|
+
start(message);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stop() {
|
|
55
|
+
if (interval) {
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
interval = null;
|
|
58
|
+
}
|
|
59
|
+
if (isRunning) {
|
|
60
|
+
clearLine();
|
|
61
|
+
isRunning = false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function succeed(message?: string) {
|
|
66
|
+
stop();
|
|
67
|
+
const text = message || currentMessage;
|
|
68
|
+
stream.write(`\x1b[32m✓\x1b[0m ${text}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fail(message?: string) {
|
|
72
|
+
stop();
|
|
73
|
+
const text = message || currentMessage;
|
|
74
|
+
stream.write(`\x1b[31m✗\x1b[0m ${text}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { start, update, succeed, fail, stop };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Simple spinner for async operations.
|
|
82
|
+
* Usage: const done = spinner.start('Processing...'); await task(); done('Done');
|
|
83
|
+
*/
|
|
84
|
+
export function spinner(stream: NodeJS.WritableStream = process.stderr) {
|
|
85
|
+
const progress = createProgress(stream);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
start(message: string) {
|
|
89
|
+
progress.start(message);
|
|
90
|
+
return (finalMessage?: string) => {
|
|
91
|
+
progress.succeed(finalMessage);
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
fail(message: string) {
|
|
95
|
+
progress.fail(message);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Progress bar for batch operations.
|
|
102
|
+
*/
|
|
103
|
+
export function progressBar(total: number, stream: NodeJS.WritableStream = process.stderr) {
|
|
104
|
+
let current = 0;
|
|
105
|
+
let lastPercent = -1;
|
|
106
|
+
|
|
107
|
+
function render(label: string) {
|
|
108
|
+
const percent = Math.floor((current / total) * 100);
|
|
109
|
+
if (percent === lastPercent) return;
|
|
110
|
+
lastPercent = percent;
|
|
111
|
+
|
|
112
|
+
const barWidth = 30;
|
|
113
|
+
const filled = Math.floor((current / total) * barWidth);
|
|
114
|
+
const empty = barWidth - filled;
|
|
115
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
116
|
+
|
|
117
|
+
stream.write(`\r\x1b[K${label} [${bar}] ${percent}% (${current}/${total})`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
start(label: string) {
|
|
122
|
+
current = 0;
|
|
123
|
+
lastPercent = -1;
|
|
124
|
+
render(label);
|
|
125
|
+
},
|
|
126
|
+
increment(label: string) {
|
|
127
|
+
current++;
|
|
128
|
+
render(label);
|
|
129
|
+
},
|
|
130
|
+
done(label: string) {
|
|
131
|
+
current = total;
|
|
132
|
+
render(label);
|
|
133
|
+
stream.write('\n');
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Format duration in human-readable form.
|
|
140
|
+
*/
|
|
141
|
+
export function formatDuration(ms: number): string {
|
|
142
|
+
if (ms < 1000) return `${ms}ms`;
|
|
143
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
144
|
+
const minutes = Math.floor(ms / 60000);
|
|
145
|
+
const seconds = Math.floor((ms % 60000) / 1000);
|
|
146
|
+
return `${minutes}m ${seconds}s`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Measure and report operation duration.
|
|
151
|
+
*/
|
|
152
|
+
export async function withProgress<T>(
|
|
153
|
+
message: string,
|
|
154
|
+
fn: () => Promise<T>,
|
|
155
|
+
stream: NodeJS.WritableStream = process.stderr
|
|
156
|
+
): Promise<T> {
|
|
157
|
+
const progress = createProgress(stream);
|
|
158
|
+
const start = Date.now();
|
|
159
|
+
|
|
160
|
+
progress.start(message);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const result = await fn();
|
|
164
|
+
const duration = formatDuration(Date.now() - start);
|
|
165
|
+
progress.succeed(`${message} (${duration})`);
|
|
166
|
+
return result;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
progress.fail(`${message} - failed`);
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize query strings for seekdb to prevent JSON parse errors.
|
|
3
|
+
*
|
|
4
|
+
* seekdb's internal parser has issues with certain characters:
|
|
5
|
+
* - Single quotes break JSON string parsing
|
|
6
|
+
* - Control characters may crash the native module
|
|
7
|
+
* - Special characters need proper escaping
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a search query string for safe use with seekdb.
|
|
12
|
+
* Removes or replaces characters that cause parse errors.
|
|
13
|
+
*/
|
|
14
|
+
export function sanitizeQuery(query: string): string {
|
|
15
|
+
if (!query || typeof query !== 'string') {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return query
|
|
20
|
+
// Remove problematic characters that break JSON parsing
|
|
21
|
+
.replace(/'/g, '') // Remove single quotes (main issue)
|
|
22
|
+
.replace(/"/g, '') // Remove double quotes
|
|
23
|
+
.replace(/\\/g, '') // Remove backslashes
|
|
24
|
+
.replace(/\x00/g, '') // Remove null bytes
|
|
25
|
+
|
|
26
|
+
// Replace control characters with spaces
|
|
27
|
+
.replace(/\n/g, ' ') // Replace newlines
|
|
28
|
+
.replace(/\r/g, ' ') // Replace carriage returns
|
|
29
|
+
.replace(/\t/g, ' ') // Replace tabs
|
|
30
|
+
|
|
31
|
+
// Collapse multiple spaces into one
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
|
|
34
|
+
// Trim leading/trailing whitespace
|
|
35
|
+
.trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Fallback search using SQL LIKE when vector search fails.
|
|
40
|
+
* More robust but less accurate than vector search.
|
|
41
|
+
*/
|
|
42
|
+
export function safeSearchPattern(query: string): string {
|
|
43
|
+
const sanitized = sanitizeQuery(query);
|
|
44
|
+
|
|
45
|
+
// For SQL LIKE, we need to escape % and _ wildcards
|
|
46
|
+
return sanitized
|
|
47
|
+
.replace(/%/g, '\\%')
|
|
48
|
+
.replace(/_/g, '\\_');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate that a query string is safe for seekdb operations.
|
|
53
|
+
* Returns true if the query is safe, false otherwise.
|
|
54
|
+
*/
|
|
55
|
+
export function isQuerySafe(query: string): boolean {
|
|
56
|
+
if (!query || query.length === 0) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for characters that cause parse errors
|
|
61
|
+
const dangerousChars = /['"\\\x00]/;
|
|
62
|
+
return !dangerousChars.test(query);
|
|
63
|
+
}
|