@timmeck/brain 1.2.0 → 1.8.1
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 +225 -50
- package/dist/api/server.d.ts +23 -0
- package/dist/api/server.js +354 -0
- package/dist/api/server.js.map +1 -0
- package/dist/brain.d.ts +3 -0
- package/dist/brain.js +46 -8
- package/dist/brain.js.map +1 -1
- package/dist/cli/commands/dashboard.js +37 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/explain.d.ts +2 -0
- package/dist/cli/commands/explain.js +76 -0
- package/dist/cli/commands/explain.js.map +1 -0
- package/dist/code/analyzer.d.ts +6 -0
- package/dist/code/analyzer.js +35 -0
- package/dist/code/analyzer.js.map +1 -1
- package/dist/code/matcher.d.ts +11 -1
- package/dist/code/matcher.js +49 -0
- package/dist/code/matcher.js.map +1 -1
- package/dist/code/scorer.d.ts +1 -0
- package/dist/code/scorer.js +15 -1
- package/dist/code/scorer.js.map +1 -1
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -1
- package/dist/dashboard/server.d.ts +15 -0
- package/dist/dashboard/server.js +124 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/db/migrations/007_feedback.d.ts +2 -0
- package/dist/db/migrations/007_feedback.js +12 -0
- package/dist/db/migrations/007_feedback.js.map +1 -0
- package/dist/db/migrations/008_git_integration.d.ts +2 -0
- package/dist/db/migrations/008_git_integration.js +37 -0
- package/dist/db/migrations/008_git_integration.js.map +1 -0
- package/dist/db/migrations/009_embeddings.d.ts +2 -0
- package/dist/db/migrations/009_embeddings.js +7 -0
- package/dist/db/migrations/009_embeddings.js.map +1 -0
- package/dist/db/migrations/index.js +6 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/repositories/code-module.repository.d.ts +16 -0
- package/dist/db/repositories/code-module.repository.js +42 -0
- package/dist/db/repositories/code-module.repository.js.map +1 -1
- package/dist/db/repositories/error.repository.d.ts +5 -0
- package/dist/db/repositories/error.repository.js +27 -0
- package/dist/db/repositories/error.repository.js.map +1 -1
- package/dist/db/repositories/insight.repository.d.ts +2 -0
- package/dist/db/repositories/insight.repository.js +13 -0
- package/dist/db/repositories/insight.repository.js.map +1 -1
- package/dist/embeddings/engine.d.ts +42 -0
- package/dist/embeddings/engine.js +179 -0
- package/dist/embeddings/engine.js.map +1 -0
- package/dist/hooks/post-tool-use.js +2 -0
- package/dist/hooks/post-tool-use.js.map +1 -1
- package/dist/hooks/post-write.js +11 -0
- package/dist/hooks/post-write.js.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ipc/router.d.ts +2 -0
- package/dist/ipc/router.js +13 -0
- package/dist/ipc/router.js.map +1 -1
- package/dist/learning/confidence-scorer.d.ts +16 -0
- package/dist/learning/confidence-scorer.js +20 -0
- package/dist/learning/confidence-scorer.js.map +1 -1
- package/dist/learning/learning-engine.js +12 -5
- package/dist/learning/learning-engine.js.map +1 -1
- package/dist/matching/error-matcher.d.ts +9 -1
- package/dist/matching/error-matcher.js +50 -5
- package/dist/matching/error-matcher.js.map +1 -1
- package/dist/mcp/http-server.d.ts +14 -0
- package/dist/mcp/http-server.js +123 -0
- package/dist/mcp/http-server.js.map +1 -0
- package/dist/mcp/tools.d.ts +4 -0
- package/dist/mcp/tools.js +41 -14
- package/dist/mcp/tools.js.map +1 -1
- package/dist/services/analytics.service.d.ts +39 -0
- package/dist/services/analytics.service.js +111 -0
- package/dist/services/analytics.service.js.map +1 -1
- package/dist/services/code.service.d.ts +5 -0
- package/dist/services/code.service.js +91 -4
- package/dist/services/code.service.js.map +1 -1
- package/dist/services/error.service.d.ts +17 -1
- package/dist/services/error.service.js +90 -12
- package/dist/services/error.service.js.map +1 -1
- package/dist/services/git.service.d.ts +49 -0
- package/dist/services/git.service.js +112 -0
- package/dist/services/git.service.js.map +1 -0
- package/dist/services/prevention.service.d.ts +7 -0
- package/dist/services/prevention.service.js +38 -0
- package/dist/services/prevention.service.js.map +1 -1
- package/dist/services/research.service.d.ts +1 -0
- package/dist/services/research.service.js +4 -0
- package/dist/services/research.service.js.map +1 -1
- package/dist/services/solution.service.d.ts +10 -0
- package/dist/services/solution.service.js +48 -0
- package/dist/services/solution.service.js.map +1 -1
- package/dist/types/config.types.d.ts +21 -0
- package/dist/types/synapse.types.d.ts +1 -1
- package/package.json +8 -3
- package/src/api/server.ts +395 -0
- package/src/brain.ts +51 -8
- package/src/cli/commands/dashboard.ts +38 -1
- package/src/cli/commands/explain.ts +83 -0
- package/src/code/analyzer.ts +40 -0
- package/src/code/matcher.ts +67 -2
- package/src/code/scorer.ts +13 -1
- package/src/config.ts +24 -0
- package/src/dashboard/server.ts +142 -0
- package/src/db/migrations/007_feedback.ts +13 -0
- package/src/db/migrations/008_git_integration.ts +38 -0
- package/src/db/migrations/009_embeddings.ts +8 -0
- package/src/db/migrations/index.ts +6 -0
- package/src/db/repositories/code-module.repository.ts +53 -0
- package/src/db/repositories/error.repository.ts +40 -0
- package/src/db/repositories/insight.repository.ts +21 -0
- package/src/embeddings/engine.ts +238 -0
- package/src/hooks/post-tool-use.ts +2 -0
- package/src/hooks/post-write.ts +12 -0
- package/src/index.ts +3 -1
- package/src/ipc/router.ts +16 -0
- package/src/learning/confidence-scorer.ts +33 -0
- package/src/learning/learning-engine.ts +13 -5
- package/src/matching/error-matcher.ts +55 -4
- package/src/mcp/http-server.ts +140 -0
- package/src/mcp/tools.ts +52 -14
- package/src/services/analytics.service.ts +136 -0
- package/src/services/code.service.ts +120 -4
- package/src/services/error.service.ts +114 -13
- package/src/services/git.service.ts +132 -0
- package/src/services/prevention.service.ts +40 -0
- package/src/services/research.service.ts +5 -0
- package/src/services/solution.service.ts +58 -0
- package/src/types/config.types.ts +24 -0
- package/src/types/synapse.types.ts +1 -0
|
@@ -19,6 +19,8 @@ export interface MatchingConfig {
|
|
|
19
19
|
fingerprintFields: string[];
|
|
20
20
|
similarityThreshold: number;
|
|
21
21
|
maxResults: number;
|
|
22
|
+
crossProjectMatching: boolean;
|
|
23
|
+
crossProjectWeight: number;
|
|
22
24
|
}
|
|
23
25
|
export interface CodeConfig {
|
|
24
26
|
supportedLanguages: string[];
|
|
@@ -55,10 +57,29 @@ export interface RetentionConfig {
|
|
|
55
57
|
solutionDays: number;
|
|
56
58
|
insightDays: number;
|
|
57
59
|
}
|
|
60
|
+
export interface ApiConfig {
|
|
61
|
+
port: number;
|
|
62
|
+
enabled: boolean;
|
|
63
|
+
apiKey?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface McpHttpConfig {
|
|
66
|
+
port: number;
|
|
67
|
+
enabled: boolean;
|
|
68
|
+
}
|
|
69
|
+
export interface EmbeddingsConfig {
|
|
70
|
+
enabled: boolean;
|
|
71
|
+
modelName: string;
|
|
72
|
+
cacheDir: string;
|
|
73
|
+
sweepIntervalMs: number;
|
|
74
|
+
batchSize: number;
|
|
75
|
+
}
|
|
58
76
|
export interface BrainConfig {
|
|
59
77
|
dataDir: string;
|
|
60
78
|
dbPath: string;
|
|
61
79
|
ipc: IpcConfig;
|
|
80
|
+
api: ApiConfig;
|
|
81
|
+
mcpHttp: McpHttpConfig;
|
|
82
|
+
embeddings: EmbeddingsConfig;
|
|
62
83
|
learning: LearningConfig;
|
|
63
84
|
terminal: TerminalConfig;
|
|
64
85
|
matching: MatchingConfig;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type NodeType = 'error' | 'solution' | 'code_module' | 'rule' | 'antipattern' | 'project' | 'insight';
|
|
2
|
-
export type SynapseType = 'solves' | 'causes' | 'similar_to' | 'uses_module' | 'derived_from' | 'co_occurs' | 'prevents' | 'improves' | 'generalizes' | 'cross_project';
|
|
2
|
+
export type SynapseType = 'solves' | 'causes' | 'similar_to' | 'uses_module' | 'depends_on' | 'derived_from' | 'co_occurs' | 'prevents' | 'improves' | 'generalizes' | 'cross_project';
|
|
3
3
|
export interface SynapseRecord {
|
|
4
4
|
id: number;
|
|
5
5
|
source_type: NodeType;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmeck/brain",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Adaptive error memory and code intelligence system with Hebbian synapse network
|
|
3
|
+
"version": "1.8.1",
|
|
4
|
+
"description": "Adaptive error memory and code intelligence system with Hebbian synapse network, hybrid search, and REST API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
"claude-code",
|
|
20
20
|
"hebbian-learning",
|
|
21
21
|
"synapse-network",
|
|
22
|
-
"adaptive-learning"
|
|
22
|
+
"adaptive-learning",
|
|
23
|
+
"vector-search",
|
|
24
|
+
"embeddings",
|
|
25
|
+
"rest-api",
|
|
26
|
+
"developer-tools"
|
|
23
27
|
],
|
|
24
28
|
"author": "Tim Mecklenburg",
|
|
25
29
|
"license": "MIT",
|
|
@@ -28,6 +32,7 @@
|
|
|
28
32
|
"url": "https://github.com/timmeck/brain"
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
35
|
+
"@huggingface/transformers": "^3.8.1",
|
|
31
36
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
32
37
|
"better-sqlite3": "^11.7.0",
|
|
33
38
|
"chalk": "^5.6.2",
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
import { getEventBus } from '../utils/events.js';
|
|
4
|
+
import type { IpcRouter } from '../ipc/router.js';
|
|
5
|
+
|
|
6
|
+
export interface ApiServerOptions {
|
|
7
|
+
port: number;
|
|
8
|
+
router: IpcRouter;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RouteDefinition {
|
|
13
|
+
method: string;
|
|
14
|
+
pattern: RegExp;
|
|
15
|
+
ipcMethod: string;
|
|
16
|
+
extractParams: (match: RegExpMatchArray, query: URLSearchParams, body?: unknown) => unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ApiServer {
|
|
20
|
+
private server: http.Server | null = null;
|
|
21
|
+
private logger = getLogger();
|
|
22
|
+
private routes: RouteDefinition[];
|
|
23
|
+
private sseClients: Set<http.ServerResponse> = new Set();
|
|
24
|
+
private statsTimer: ReturnType<typeof setInterval> | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(private options: ApiServerOptions) {
|
|
27
|
+
this.routes = this.buildRoutes();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
start(): void {
|
|
31
|
+
const { port, apiKey } = this.options;
|
|
32
|
+
|
|
33
|
+
this.server = http.createServer((req, res) => {
|
|
34
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
35
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
36
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
|
37
|
+
|
|
38
|
+
if (req.method === 'OPTIONS') {
|
|
39
|
+
res.writeHead(204);
|
|
40
|
+
res.end();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (apiKey) {
|
|
45
|
+
const provided = (req.headers['x-api-key'] as string) ??
|
|
46
|
+
req.headers.authorization?.replace('Bearer ', '');
|
|
47
|
+
if (provided !== apiKey) {
|
|
48
|
+
this.json(res, 401, { error: 'Unauthorized', message: 'Invalid or missing API key' });
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.handleRequest(req, res).catch((err) => {
|
|
54
|
+
this.logger.error('API error:', err);
|
|
55
|
+
this.json(res, 500, {
|
|
56
|
+
error: 'Internal Server Error',
|
|
57
|
+
message: err instanceof Error ? err.message : String(err),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this.server.listen(port, () => {
|
|
63
|
+
this.logger.info(`REST API server started on http://localhost:${port}`);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.setupSSE();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stop(): void {
|
|
70
|
+
if (this.statsTimer) {
|
|
71
|
+
clearInterval(this.statsTimer);
|
|
72
|
+
this.statsTimer = null;
|
|
73
|
+
}
|
|
74
|
+
for (const client of this.sseClients) {
|
|
75
|
+
try { client.end(); } catch { /* ignore */ }
|
|
76
|
+
}
|
|
77
|
+
this.sseClients.clear();
|
|
78
|
+
this.server?.close();
|
|
79
|
+
this.server = null;
|
|
80
|
+
this.logger.info('REST API server stopped');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
84
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
85
|
+
const pathname = url.pathname;
|
|
86
|
+
const method = req.method ?? 'GET';
|
|
87
|
+
const query = url.searchParams;
|
|
88
|
+
|
|
89
|
+
// Health check
|
|
90
|
+
if (pathname === '/api/v1/health') {
|
|
91
|
+
this.json(res, 200, { status: 'ok', timestamp: new Date().toISOString() });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// SSE event stream (for live dashboard)
|
|
96
|
+
if (pathname === '/api/v1/events' && method === 'GET') {
|
|
97
|
+
res.writeHead(200, {
|
|
98
|
+
'Content-Type': 'text/event-stream',
|
|
99
|
+
'Cache-Control': 'no-cache',
|
|
100
|
+
'Connection': 'keep-alive',
|
|
101
|
+
});
|
|
102
|
+
res.write('data: {"type":"connected"}\n\n');
|
|
103
|
+
this.sseClients.add(res);
|
|
104
|
+
req.on('close', () => this.sseClients.delete(res));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// List all available methods
|
|
109
|
+
if (pathname === '/api/v1/methods' && method === 'GET') {
|
|
110
|
+
const methods = this.options.router.listMethods();
|
|
111
|
+
this.json(res, 200, {
|
|
112
|
+
methods,
|
|
113
|
+
rpcEndpoint: '/api/v1/rpc',
|
|
114
|
+
usage: 'POST /api/v1/rpc with body { "method": "<method>", "params": {...} }',
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Generic RPC endpoint — the universal gateway
|
|
120
|
+
if (pathname === '/api/v1/rpc' && method === 'POST') {
|
|
121
|
+
const body = await this.readBody(req);
|
|
122
|
+
if (!body) {
|
|
123
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Empty request body' });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const parsed = JSON.parse(body);
|
|
128
|
+
|
|
129
|
+
// Batch RPC support
|
|
130
|
+
if (Array.isArray(parsed)) {
|
|
131
|
+
const results = parsed.map((call: { method: string; params?: unknown; id?: string | number }) => {
|
|
132
|
+
try {
|
|
133
|
+
const result = this.options.router.handle(call.method, call.params ?? {});
|
|
134
|
+
return { id: call.id, result };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
return { id: call.id, error: err instanceof Error ? err.message : String(err) };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
this.json(res, 200, results);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!parsed.method) {
|
|
144
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Missing "method" field' });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const result = this.options.router.handle(parsed.method, parsed.params ?? {});
|
|
150
|
+
this.json(res, 200, { result });
|
|
151
|
+
} catch (err) {
|
|
152
|
+
this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// RESTful routes
|
|
158
|
+
let body: unknown = undefined;
|
|
159
|
+
if (method === 'POST' || method === 'PUT') {
|
|
160
|
+
try {
|
|
161
|
+
const raw = await this.readBody(req);
|
|
162
|
+
body = raw ? JSON.parse(raw) : {};
|
|
163
|
+
} catch {
|
|
164
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Invalid JSON body' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const route of this.routes) {
|
|
170
|
+
if (route.method !== method) continue;
|
|
171
|
+
const match = pathname.match(route.pattern);
|
|
172
|
+
if (!match) continue;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const params = route.extractParams(match, query, body);
|
|
176
|
+
const result = this.options.router.handle(route.ipcMethod, params);
|
|
177
|
+
this.json(res, method === 'POST' ? 201 : 200, { result });
|
|
178
|
+
} catch (err) {
|
|
179
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
180
|
+
const status = msg.startsWith('Unknown method') ? 404 : 400;
|
|
181
|
+
this.json(res, status, { error: msg });
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.json(res, 404, { error: 'Not Found', message: `No route for ${method} ${pathname}` });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private buildRoutes(): RouteDefinition[] {
|
|
190
|
+
return [
|
|
191
|
+
// ─── Errors ────────────────────────────────────────────
|
|
192
|
+
{ method: 'POST', pattern: /^\/api\/v1\/errors$/, ipcMethod: 'error.report',
|
|
193
|
+
extractParams: (_m, _q, body) => body },
|
|
194
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors$/, ipcMethod: 'error.query',
|
|
195
|
+
extractParams: (_m, q) => ({
|
|
196
|
+
search: q.get('search') ?? '',
|
|
197
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
198
|
+
}) },
|
|
199
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors\/(\d+)$/, ipcMethod: 'error.get',
|
|
200
|
+
extractParams: (m) => ({ id: Number(m[1]) }) },
|
|
201
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors\/(\d+)\/match$/, ipcMethod: 'error.match',
|
|
202
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
203
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors\/(\d+)\/chain$/, ipcMethod: 'error.chain',
|
|
204
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
205
|
+
{ method: 'POST', pattern: /^\/api\/v1\/errors\/(\d+)\/resolve$/, ipcMethod: 'error.resolve',
|
|
206
|
+
extractParams: (m, _q, body) => ({ errorId: Number(m[1]), ...(body as object) }) },
|
|
207
|
+
|
|
208
|
+
// ─── Solutions ─────────────────────────────────────────
|
|
209
|
+
{ method: 'POST', pattern: /^\/api\/v1\/solutions$/, ipcMethod: 'solution.report',
|
|
210
|
+
extractParams: (_m, _q, body) => body },
|
|
211
|
+
{ method: 'GET', pattern: /^\/api\/v1\/solutions$/, ipcMethod: 'solution.query',
|
|
212
|
+
extractParams: (_m, q) => ({
|
|
213
|
+
errorId: q.get('errorId') ? Number(q.get('errorId')) : undefined,
|
|
214
|
+
}) },
|
|
215
|
+
{ method: 'POST', pattern: /^\/api\/v1\/solutions\/rate$/, ipcMethod: 'solution.rate',
|
|
216
|
+
extractParams: (_m, _q, body) => body },
|
|
217
|
+
{ method: 'GET', pattern: /^\/api\/v1\/solutions\/efficiency$/, ipcMethod: 'solution.efficiency',
|
|
218
|
+
extractParams: () => ({}) },
|
|
219
|
+
|
|
220
|
+
// ─── Projects ──────────────────────────────────────────
|
|
221
|
+
{ method: 'GET', pattern: /^\/api\/v1\/projects$/, ipcMethod: 'project.list',
|
|
222
|
+
extractParams: () => ({}) },
|
|
223
|
+
|
|
224
|
+
// ─── Code ──────────────────────────────────────────────
|
|
225
|
+
{ method: 'POST', pattern: /^\/api\/v1\/code\/analyze$/, ipcMethod: 'code.analyze',
|
|
226
|
+
extractParams: (_m, _q, body) => body },
|
|
227
|
+
{ method: 'POST', pattern: /^\/api\/v1\/code\/find$/, ipcMethod: 'code.find',
|
|
228
|
+
extractParams: (_m, _q, body) => body },
|
|
229
|
+
{ method: 'POST', pattern: /^\/api\/v1\/code\/similarity$/, ipcMethod: 'code.similarity',
|
|
230
|
+
extractParams: (_m, _q, body) => body },
|
|
231
|
+
{ method: 'GET', pattern: /^\/api\/v1\/code\/modules$/, ipcMethod: 'code.modules',
|
|
232
|
+
extractParams: (_m, q) => ({
|
|
233
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
234
|
+
language: q.get('language') ?? undefined,
|
|
235
|
+
limit: q.get('limit') ? Number(q.get('limit')) : undefined,
|
|
236
|
+
}) },
|
|
237
|
+
{ method: 'GET', pattern: /^\/api\/v1\/code\/(\d+)$/, ipcMethod: 'code.get',
|
|
238
|
+
extractParams: (m) => ({ id: Number(m[1]) }) },
|
|
239
|
+
|
|
240
|
+
// ─── Prevention ────────────────────────────────────────
|
|
241
|
+
{ method: 'POST', pattern: /^\/api\/v1\/prevention\/check$/, ipcMethod: 'prevention.check',
|
|
242
|
+
extractParams: (_m, _q, body) => body },
|
|
243
|
+
{ method: 'POST', pattern: /^\/api\/v1\/prevention\/antipatterns$/, ipcMethod: 'prevention.antipatterns',
|
|
244
|
+
extractParams: (_m, _q, body) => body },
|
|
245
|
+
{ method: 'POST', pattern: /^\/api\/v1\/prevention\/code$/, ipcMethod: 'prevention.checkCode',
|
|
246
|
+
extractParams: (_m, _q, body) => body },
|
|
247
|
+
|
|
248
|
+
// ─── Synapses ─────────────────────────────────────────
|
|
249
|
+
{ method: 'GET', pattern: /^\/api\/v1\/synapses\/context\/(\d+)$/, ipcMethod: 'synapse.context',
|
|
250
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
251
|
+
{ method: 'POST', pattern: /^\/api\/v1\/synapses\/path$/, ipcMethod: 'synapse.path',
|
|
252
|
+
extractParams: (_m, _q, body) => body },
|
|
253
|
+
{ method: 'POST', pattern: /^\/api\/v1\/synapses\/related$/, ipcMethod: 'synapse.related',
|
|
254
|
+
extractParams: (_m, _q, body) => body },
|
|
255
|
+
{ method: 'GET', pattern: /^\/api\/v1\/synapses\/stats$/, ipcMethod: 'synapse.stats',
|
|
256
|
+
extractParams: () => ({}) },
|
|
257
|
+
|
|
258
|
+
// ─── Research ──────────────────────────────────────────
|
|
259
|
+
{ method: 'GET', pattern: /^\/api\/v1\/research\/insights$/, ipcMethod: 'research.insights',
|
|
260
|
+
extractParams: (_m, q) => ({
|
|
261
|
+
type: q.get('type') ?? undefined,
|
|
262
|
+
limit: q.get('limit') ? Number(q.get('limit')) : 20,
|
|
263
|
+
activeOnly: q.get('activeOnly') !== 'false',
|
|
264
|
+
}) },
|
|
265
|
+
{ method: 'POST', pattern: /^\/api\/v1\/research\/insights\/(\d+)\/rate$/, ipcMethod: 'insight.rate',
|
|
266
|
+
extractParams: (m, _q, body) => ({ id: Number(m[1]), ...(body as object) }) },
|
|
267
|
+
{ method: 'GET', pattern: /^\/api\/v1\/research\/suggest$/, ipcMethod: 'research.suggest',
|
|
268
|
+
extractParams: (_m, q) => ({
|
|
269
|
+
context: q.get('context') ?? '',
|
|
270
|
+
limit: 10,
|
|
271
|
+
activeOnly: true,
|
|
272
|
+
}) },
|
|
273
|
+
{ method: 'GET', pattern: /^\/api\/v1\/research\/trends$/, ipcMethod: 'research.trends',
|
|
274
|
+
extractParams: (_m, q) => ({
|
|
275
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
276
|
+
windowDays: q.get('windowDays') ? Number(q.get('windowDays')) : undefined,
|
|
277
|
+
}) },
|
|
278
|
+
|
|
279
|
+
// ─── Notifications ────────────────────────────────────
|
|
280
|
+
{ method: 'GET', pattern: /^\/api\/v1\/notifications$/, ipcMethod: 'notification.list',
|
|
281
|
+
extractParams: (_m, q) => ({
|
|
282
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
283
|
+
}) },
|
|
284
|
+
{ method: 'POST', pattern: /^\/api\/v1\/notifications\/(\d+)\/ack$/, ipcMethod: 'notification.ack',
|
|
285
|
+
extractParams: (m) => ({ id: Number(m[1]) }) },
|
|
286
|
+
|
|
287
|
+
// ─── Analytics ─────────────────────────────────────────
|
|
288
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/summary$/, ipcMethod: 'analytics.summary',
|
|
289
|
+
extractParams: (_m, q) => ({
|
|
290
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
291
|
+
}) },
|
|
292
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/network$/, ipcMethod: 'analytics.network',
|
|
293
|
+
extractParams: (_m, q) => ({
|
|
294
|
+
limit: q.get('limit') ? Number(q.get('limit')) : undefined,
|
|
295
|
+
}) },
|
|
296
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/health$/, ipcMethod: 'analytics.health',
|
|
297
|
+
extractParams: (_m, q) => ({
|
|
298
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
299
|
+
}) },
|
|
300
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/timeline$/, ipcMethod: 'analytics.timeline',
|
|
301
|
+
extractParams: (_m, q) => ({
|
|
302
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
303
|
+
days: q.get('days') ? Number(q.get('days')) : undefined,
|
|
304
|
+
}) },
|
|
305
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/explain\/(\d+)$/, ipcMethod: 'analytics.explain',
|
|
306
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
307
|
+
|
|
308
|
+
// ─── Git ───────────────────────────────────────────────
|
|
309
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/context$/, ipcMethod: 'git.context',
|
|
310
|
+
extractParams: (_m, q) => ({ cwd: q.get('cwd') ?? undefined }) },
|
|
311
|
+
{ method: 'POST', pattern: /^\/api\/v1\/git\/link-error$/, ipcMethod: 'git.linkError',
|
|
312
|
+
extractParams: (_m, _q, body) => body },
|
|
313
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/errors\/(\d+)\/commits$/, ipcMethod: 'git.errorCommits',
|
|
314
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
315
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/commits\/([a-f0-9]+)\/errors$/, ipcMethod: 'git.commitErrors',
|
|
316
|
+
extractParams: (m) => ({ commitHash: m[1] }) },
|
|
317
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/diff$/, ipcMethod: 'git.diff',
|
|
318
|
+
extractParams: (_m, q) => ({ cwd: q.get('cwd') ?? undefined }) },
|
|
319
|
+
|
|
320
|
+
// ─── Terminal ──────────────────────────────────────────
|
|
321
|
+
{ method: 'POST', pattern: /^\/api\/v1\/terminal\/register$/, ipcMethod: 'terminal.register',
|
|
322
|
+
extractParams: (_m, _q, body) => body },
|
|
323
|
+
{ method: 'POST', pattern: /^\/api\/v1\/terminal\/heartbeat$/, ipcMethod: 'terminal.heartbeat',
|
|
324
|
+
extractParams: (_m, _q, body) => body },
|
|
325
|
+
{ method: 'POST', pattern: /^\/api\/v1\/terminal\/disconnect$/, ipcMethod: 'terminal.disconnect',
|
|
326
|
+
extractParams: (_m, _q, body) => body },
|
|
327
|
+
|
|
328
|
+
// ─── Learning ──────────────────────────────────────────
|
|
329
|
+
{ method: 'POST', pattern: /^\/api\/v1\/learning\/run$/, ipcMethod: 'learning.run',
|
|
330
|
+
extractParams: () => ({}) },
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private json(res: http.ServerResponse, status: number, data: unknown): void {
|
|
335
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
336
|
+
res.end(JSON.stringify(data));
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private readBody(req: http.IncomingMessage): Promise<string> {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const chunks: Buffer[] = [];
|
|
342
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
343
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
344
|
+
req.on('error', reject);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private setupSSE(): void {
|
|
349
|
+
const bus = getEventBus();
|
|
350
|
+
const eventNames = [
|
|
351
|
+
'error:reported', 'error:resolved', 'solution:applied',
|
|
352
|
+
'solution:created', 'module:registered', 'module:updated',
|
|
353
|
+
'synapse:created', 'synapse:strengthened',
|
|
354
|
+
'insight:created', 'rule:learned',
|
|
355
|
+
] as const;
|
|
356
|
+
|
|
357
|
+
for (const eventName of eventNames) {
|
|
358
|
+
bus.on(eventName, (data: unknown) => {
|
|
359
|
+
this.broadcastSSE({ type: 'event', event: eventName, data });
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Periodic stats broadcast every 30s
|
|
364
|
+
this.statsTimer = setInterval(() => {
|
|
365
|
+
if (this.sseClients.size > 0) {
|
|
366
|
+
try {
|
|
367
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
368
|
+
const summary = this.options.router.handle('analytics.summary', {}) as any;
|
|
369
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
370
|
+
const network = this.options.router.handle('synapse.stats', {}) as any;
|
|
371
|
+
const stats = {
|
|
372
|
+
modules: summary?.modules?.total ?? 0,
|
|
373
|
+
synapses: network?.totalSynapses ?? 0,
|
|
374
|
+
errors: summary?.errors?.total ?? 0,
|
|
375
|
+
solutions: summary?.solutions?.total ?? 0,
|
|
376
|
+
rules: summary?.rules?.active ?? 0,
|
|
377
|
+
insights: summary?.insights?.total ?? 0,
|
|
378
|
+
};
|
|
379
|
+
this.broadcastSSE({ type: 'stats_update', stats });
|
|
380
|
+
} catch { /* ignore stats errors */ }
|
|
381
|
+
}
|
|
382
|
+
}, 30_000);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private broadcastSSE(data: unknown): void {
|
|
386
|
+
const msg = `data: ${JSON.stringify(data)}\n\n`;
|
|
387
|
+
for (const client of this.sseClients) {
|
|
388
|
+
try {
|
|
389
|
+
client.write(msg);
|
|
390
|
+
} catch {
|
|
391
|
+
this.sseClients.delete(client);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
package/src/brain.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { SynapseService } from './services/synapse.service.js';
|
|
|
30
30
|
import { ResearchService } from './services/research.service.js';
|
|
31
31
|
import { NotificationService } from './services/notification.service.js';
|
|
32
32
|
import { AnalyticsService } from './services/analytics.service.js';
|
|
33
|
+
import { GitService } from './services/git.service.js';
|
|
33
34
|
|
|
34
35
|
// Synapses
|
|
35
36
|
import { SynapseManager } from './synapses/synapse-manager.js';
|
|
@@ -42,9 +43,19 @@ import { ResearchEngine } from './research/research-engine.js';
|
|
|
42
43
|
import { IpcRouter, type Services } from './ipc/router.js';
|
|
43
44
|
import { IpcServer } from './ipc/server.js';
|
|
44
45
|
|
|
46
|
+
// API & MCP HTTP
|
|
47
|
+
import { ApiServer } from './api/server.js';
|
|
48
|
+
import { McpHttpServer } from './mcp/http-server.js';
|
|
49
|
+
|
|
50
|
+
// Embeddings
|
|
51
|
+
import { EmbeddingEngine } from './embeddings/engine.js';
|
|
52
|
+
|
|
45
53
|
export class BrainCore {
|
|
46
54
|
private db: Database.Database | null = null;
|
|
47
55
|
private ipcServer: IpcServer | null = null;
|
|
56
|
+
private apiServer: ApiServer | null = null;
|
|
57
|
+
private mcpHttpServer: McpHttpServer | null = null;
|
|
58
|
+
private embeddingEngine: EmbeddingEngine | null = null;
|
|
48
59
|
private learningEngine: LearningEngine | null = null;
|
|
49
60
|
private researchEngine: ResearchEngine | null = null;
|
|
50
61
|
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
@@ -89,7 +100,7 @@ export class BrainCore {
|
|
|
89
100
|
|
|
90
101
|
// 7. Services
|
|
91
102
|
const services: Services = {
|
|
92
|
-
error: new ErrorService(errorRepo, projectRepo, synapseManager),
|
|
103
|
+
error: new ErrorService(errorRepo, projectRepo, synapseManager, config.matching),
|
|
93
104
|
solution: new SolutionService(solutionRepo, synapseManager),
|
|
94
105
|
terminal: new TerminalService(terminalRepo, config.terminal.staleTimeout),
|
|
95
106
|
prevention: new PreventionService(ruleRepo, antipatternRepo, synapseManager),
|
|
@@ -102,9 +113,20 @@ export class BrainCore {
|
|
|
102
113
|
ruleRepo, antipatternRepo, insightRepo,
|
|
103
114
|
synapseManager,
|
|
104
115
|
),
|
|
116
|
+
git: new GitService(this.db!, synapseManager),
|
|
105
117
|
};
|
|
106
118
|
|
|
107
|
-
// 8.
|
|
119
|
+
// 8. Embedding Engine (local vector search)
|
|
120
|
+
if (config.embeddings.enabled) {
|
|
121
|
+
this.embeddingEngine = new EmbeddingEngine(config.embeddings, this.db!);
|
|
122
|
+
this.embeddingEngine.start();
|
|
123
|
+
// Wire embedding engine into services for hybrid search
|
|
124
|
+
services.error.setEmbeddingEngine(this.embeddingEngine);
|
|
125
|
+
services.code.setEmbeddingEngine(this.embeddingEngine);
|
|
126
|
+
logger.info('Embedding engine started (model will load in background)');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 9. Learning Engine
|
|
108
130
|
this.learningEngine = new LearningEngine(
|
|
109
131
|
config.learning, errorRepo, solutionRepo,
|
|
110
132
|
ruleRepo, antipatternRepo, synapseManager,
|
|
@@ -112,7 +134,7 @@ export class BrainCore {
|
|
|
112
134
|
this.learningEngine.start();
|
|
113
135
|
logger.info(`Learning engine started (interval: ${config.learning.intervalMs}ms)`);
|
|
114
136
|
|
|
115
|
-
//
|
|
137
|
+
// 10. Research Engine
|
|
116
138
|
this.researchEngine = new ResearchEngine(
|
|
117
139
|
config.research, errorRepo, solutionRepo, projectRepo,
|
|
118
140
|
codeModuleRepo, synapseRepo, insightRepo, synapseManager,
|
|
@@ -123,24 +145,42 @@ export class BrainCore {
|
|
|
123
145
|
// Expose learning engine to IPC
|
|
124
146
|
services.learning = this.learningEngine;
|
|
125
147
|
|
|
126
|
-
//
|
|
148
|
+
// 11. IPC Server
|
|
127
149
|
const router = new IpcRouter(services);
|
|
128
150
|
this.ipcServer = new IpcServer(router, config.ipc.pipeName);
|
|
129
151
|
this.ipcServer.start();
|
|
130
152
|
|
|
131
|
-
//
|
|
153
|
+
// 11a. REST API Server
|
|
154
|
+
if (config.api.enabled) {
|
|
155
|
+
this.apiServer = new ApiServer({
|
|
156
|
+
port: config.api.port,
|
|
157
|
+
router,
|
|
158
|
+
apiKey: config.api.apiKey,
|
|
159
|
+
});
|
|
160
|
+
this.apiServer.start();
|
|
161
|
+
logger.info(`REST API enabled on port ${config.api.port}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 11b. MCP HTTP Server (SSE transport for Cursor, Windsurf, Cline, Continue)
|
|
165
|
+
if (config.mcpHttp.enabled) {
|
|
166
|
+
this.mcpHttpServer = new McpHttpServer(config.mcpHttp.port, router);
|
|
167
|
+
this.mcpHttpServer.start();
|
|
168
|
+
logger.info(`MCP HTTP (SSE) enabled on port ${config.mcpHttp.port}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 12. Terminal cleanup timer
|
|
132
172
|
this.cleanupTimer = setInterval(() => {
|
|
133
173
|
services.terminal.cleanup();
|
|
134
174
|
}, 60_000);
|
|
135
175
|
|
|
136
|
-
//
|
|
176
|
+
// 13. Event listeners (synapse wiring)
|
|
137
177
|
this.setupEventListeners(services, synapseManager);
|
|
138
178
|
|
|
139
|
-
//
|
|
179
|
+
// 14. PID file
|
|
140
180
|
const pidPath = path.join(path.dirname(config.dbPath), 'brain.pid');
|
|
141
181
|
fs.writeFileSync(pidPath, String(process.pid));
|
|
142
182
|
|
|
143
|
-
//
|
|
183
|
+
// 15. Graceful shutdown
|
|
144
184
|
process.on('SIGINT', () => this.stop());
|
|
145
185
|
process.on('SIGTERM', () => this.stop());
|
|
146
186
|
|
|
@@ -157,7 +197,10 @@ export class BrainCore {
|
|
|
157
197
|
}
|
|
158
198
|
|
|
159
199
|
this.researchEngine?.stop();
|
|
200
|
+
this.embeddingEngine?.stop();
|
|
160
201
|
this.learningEngine?.stop();
|
|
202
|
+
this.mcpHttpServer?.stop();
|
|
203
|
+
this.apiServer?.stop();
|
|
161
204
|
this.ipcServer?.stop();
|
|
162
205
|
this.db?.close();
|
|
163
206
|
|
|
@@ -9,6 +9,8 @@ export function dashboardCommand(): Command {
|
|
|
9
9
|
.description('Generate and open the Brain dashboard with live data')
|
|
10
10
|
.option('-o, --output <path>', 'Output HTML file path')
|
|
11
11
|
.option('--no-open', 'Generate without opening in browser')
|
|
12
|
+
.option('-l, --live', 'Start live dashboard server with SSE updates')
|
|
13
|
+
.option('-p, --port <number>', 'Port for live dashboard', '7420')
|
|
12
14
|
.action(async (opts) => {
|
|
13
15
|
await withIpc(async (client) => {
|
|
14
16
|
console.log(`${icons.chart} ${c.info('Fetching data from Brain...')}`);
|
|
@@ -69,8 +71,43 @@ export function dashboardCommand(): Command {
|
|
|
69
71
|
? resolve(opts.output)
|
|
70
72
|
: resolve(import.meta.dirname, '../../../dashboard.html');
|
|
71
73
|
|
|
72
|
-
|
|
74
|
+
// Inject live SSE connection for --live mode
|
|
75
|
+
let finalHtml = html;
|
|
76
|
+
if (opts.live) {
|
|
77
|
+
const apiPort = opts.port || '7777';
|
|
78
|
+
const sseScript = `
|
|
79
|
+
<script>
|
|
80
|
+
(function(){
|
|
81
|
+
const evtSource = new EventSource('http://localhost:${apiPort}/api/v1/events');
|
|
82
|
+
evtSource.onmessage = function(e) {
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(e.data);
|
|
85
|
+
if (data.type === 'stats_update') {
|
|
86
|
+
document.querySelectorAll('.stat-card').forEach(card => {
|
|
87
|
+
const label = card.querySelector('.stat-label')?.textContent?.toLowerCase();
|
|
88
|
+
const num = card.querySelector('.stat-number');
|
|
89
|
+
if (label && num && data.stats[label] !== undefined) {
|
|
90
|
+
num.textContent = Number(data.stats[label]).toLocaleString();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (data.type === 'event') {
|
|
95
|
+
const dot = document.querySelector('.activity-dot');
|
|
96
|
+
if (dot) { dot.style.background = '#ff5577'; setTimeout(() => dot.style.background = '', 500); }
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
99
|
+
};
|
|
100
|
+
evtSource.onerror = function() { setTimeout(() => location.reload(), 5000); };
|
|
101
|
+
})();
|
|
102
|
+
</script>`;
|
|
103
|
+
finalHtml = html.replace('</body>', sseScript + '</body>');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
writeFileSync(outPath, finalHtml, 'utf-8');
|
|
73
107
|
console.log(`${icons.ok} ${c.success('Dashboard written to')} ${c.dim(outPath)}`);
|
|
108
|
+
if (opts.live) {
|
|
109
|
+
console.log(` ${c.info('Live mode:')} Connected to Brain daemon SSE on port ${opts.port || 7777}`);
|
|
110
|
+
}
|
|
74
111
|
console.log(` ${c.label('Modules:')} ${c.value(data.stats.modules)} ${c.label('Synapses:')} ${c.value(data.stats.synapses)} ${c.label('Insights:')} ${c.value(data.stats.insights)}`);
|
|
75
112
|
|
|
76
113
|
if (opts.open !== false) {
|