@timmeck/brain 1.2.0 → 1.8.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 +225 -50
- package/dist/api/server.d.ts +19 -0
- package/dist/api/server.js +281 -0
- package/dist/api/server.js.map +1 -0
- package/dist/brain.d.ts +3 -0
- package/dist/brain.js +45 -8
- package/dist/brain.js.map +1 -1
- package/dist/cli/commands/dashboard.js +2 -0
- 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 +166 -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 +117 -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 +2 -0
- package/dist/services/code.service.js +62 -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 +321 -0
- package/src/brain.ts +50 -8
- package/src/cli/commands/dashboard.ts +2 -0
- 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 +217 -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 +137 -0
- package/src/mcp/tools.ts +52 -14
- package/src/services/analytics.service.ts +136 -0
- package/src/services/code.service.ts +87 -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
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { getLogger } from '../utils/logger.js';
|
|
3
|
+
import type { IpcRouter } from '../ipc/router.js';
|
|
4
|
+
|
|
5
|
+
export interface ApiServerOptions {
|
|
6
|
+
port: number;
|
|
7
|
+
router: IpcRouter;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RouteDefinition {
|
|
12
|
+
method: string;
|
|
13
|
+
pattern: RegExp;
|
|
14
|
+
ipcMethod: string;
|
|
15
|
+
extractParams: (match: RegExpMatchArray, query: URLSearchParams, body?: unknown) => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ApiServer {
|
|
19
|
+
private server: http.Server | null = null;
|
|
20
|
+
private logger = getLogger();
|
|
21
|
+
private routes: RouteDefinition[];
|
|
22
|
+
|
|
23
|
+
constructor(private options: ApiServerOptions) {
|
|
24
|
+
this.routes = this.buildRoutes();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
start(): void {
|
|
28
|
+
const { port, apiKey } = this.options;
|
|
29
|
+
|
|
30
|
+
this.server = http.createServer((req, res) => {
|
|
31
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
32
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
33
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
|
34
|
+
|
|
35
|
+
if (req.method === 'OPTIONS') {
|
|
36
|
+
res.writeHead(204);
|
|
37
|
+
res.end();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (apiKey) {
|
|
42
|
+
const provided = (req.headers['x-api-key'] as string) ??
|
|
43
|
+
req.headers.authorization?.replace('Bearer ', '');
|
|
44
|
+
if (provided !== apiKey) {
|
|
45
|
+
this.json(res, 401, { error: 'Unauthorized', message: 'Invalid or missing API key' });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
this.handleRequest(req, res).catch((err) => {
|
|
51
|
+
this.logger.error('API error:', err);
|
|
52
|
+
this.json(res, 500, {
|
|
53
|
+
error: 'Internal Server Error',
|
|
54
|
+
message: err instanceof Error ? err.message : String(err),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.server.listen(port, () => {
|
|
60
|
+
this.logger.info(`REST API server started on http://localhost:${port}`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
stop(): void {
|
|
65
|
+
this.server?.close();
|
|
66
|
+
this.server = null;
|
|
67
|
+
this.logger.info('REST API server stopped');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
|
71
|
+
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
72
|
+
const pathname = url.pathname;
|
|
73
|
+
const method = req.method ?? 'GET';
|
|
74
|
+
const query = url.searchParams;
|
|
75
|
+
|
|
76
|
+
// Health check
|
|
77
|
+
if (pathname === '/api/v1/health') {
|
|
78
|
+
this.json(res, 200, { status: 'ok', timestamp: new Date().toISOString() });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// List all available methods
|
|
83
|
+
if (pathname === '/api/v1/methods' && method === 'GET') {
|
|
84
|
+
const methods = this.options.router.listMethods();
|
|
85
|
+
this.json(res, 200, {
|
|
86
|
+
methods,
|
|
87
|
+
rpcEndpoint: '/api/v1/rpc',
|
|
88
|
+
usage: 'POST /api/v1/rpc with body { "method": "<method>", "params": {...} }',
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generic RPC endpoint — the universal gateway
|
|
94
|
+
if (pathname === '/api/v1/rpc' && method === 'POST') {
|
|
95
|
+
const body = await this.readBody(req);
|
|
96
|
+
if (!body) {
|
|
97
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Empty request body' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parsed = JSON.parse(body);
|
|
102
|
+
|
|
103
|
+
// Batch RPC support
|
|
104
|
+
if (Array.isArray(parsed)) {
|
|
105
|
+
const results = parsed.map((call: { method: string; params?: unknown; id?: string | number }) => {
|
|
106
|
+
try {
|
|
107
|
+
const result = this.options.router.handle(call.method, call.params ?? {});
|
|
108
|
+
return { id: call.id, result };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { id: call.id, error: err instanceof Error ? err.message : String(err) };
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
this.json(res, 200, results);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!parsed.method) {
|
|
118
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Missing "method" field' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = this.options.router.handle(parsed.method, parsed.params ?? {});
|
|
124
|
+
this.json(res, 200, { result });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
this.json(res, 400, { error: err instanceof Error ? err.message : String(err) });
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// RESTful routes
|
|
132
|
+
let body: unknown = undefined;
|
|
133
|
+
if (method === 'POST' || method === 'PUT') {
|
|
134
|
+
try {
|
|
135
|
+
const raw = await this.readBody(req);
|
|
136
|
+
body = raw ? JSON.parse(raw) : {};
|
|
137
|
+
} catch {
|
|
138
|
+
this.json(res, 400, { error: 'Bad Request', message: 'Invalid JSON body' });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for (const route of this.routes) {
|
|
144
|
+
if (route.method !== method) continue;
|
|
145
|
+
const match = pathname.match(route.pattern);
|
|
146
|
+
if (!match) continue;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const params = route.extractParams(match, query, body);
|
|
150
|
+
const result = this.options.router.handle(route.ipcMethod, params);
|
|
151
|
+
this.json(res, method === 'POST' ? 201 : 200, { result });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
154
|
+
const status = msg.startsWith('Unknown method') ? 404 : 400;
|
|
155
|
+
this.json(res, status, { error: msg });
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
this.json(res, 404, { error: 'Not Found', message: `No route for ${method} ${pathname}` });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private buildRoutes(): RouteDefinition[] {
|
|
164
|
+
return [
|
|
165
|
+
// ─── Errors ────────────────────────────────────────────
|
|
166
|
+
{ method: 'POST', pattern: /^\/api\/v1\/errors$/, ipcMethod: 'error.report',
|
|
167
|
+
extractParams: (_m, _q, body) => body },
|
|
168
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors$/, ipcMethod: 'error.query',
|
|
169
|
+
extractParams: (_m, q) => ({
|
|
170
|
+
search: q.get('search') ?? '',
|
|
171
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
172
|
+
}) },
|
|
173
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors\/(\d+)$/, ipcMethod: 'error.get',
|
|
174
|
+
extractParams: (m) => ({ id: Number(m[1]) }) },
|
|
175
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors\/(\d+)\/match$/, ipcMethod: 'error.match',
|
|
176
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
177
|
+
{ method: 'GET', pattern: /^\/api\/v1\/errors\/(\d+)\/chain$/, ipcMethod: 'error.chain',
|
|
178
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
179
|
+
{ method: 'POST', pattern: /^\/api\/v1\/errors\/(\d+)\/resolve$/, ipcMethod: 'error.resolve',
|
|
180
|
+
extractParams: (m, _q, body) => ({ errorId: Number(m[1]), ...(body as object) }) },
|
|
181
|
+
|
|
182
|
+
// ─── Solutions ─────────────────────────────────────────
|
|
183
|
+
{ method: 'POST', pattern: /^\/api\/v1\/solutions$/, ipcMethod: 'solution.report',
|
|
184
|
+
extractParams: (_m, _q, body) => body },
|
|
185
|
+
{ method: 'GET', pattern: /^\/api\/v1\/solutions$/, ipcMethod: 'solution.query',
|
|
186
|
+
extractParams: (_m, q) => ({
|
|
187
|
+
errorId: q.get('errorId') ? Number(q.get('errorId')) : undefined,
|
|
188
|
+
}) },
|
|
189
|
+
{ method: 'POST', pattern: /^\/api\/v1\/solutions\/rate$/, ipcMethod: 'solution.rate',
|
|
190
|
+
extractParams: (_m, _q, body) => body },
|
|
191
|
+
{ method: 'GET', pattern: /^\/api\/v1\/solutions\/efficiency$/, ipcMethod: 'solution.efficiency',
|
|
192
|
+
extractParams: () => ({}) },
|
|
193
|
+
|
|
194
|
+
// ─── Projects ──────────────────────────────────────────
|
|
195
|
+
{ method: 'GET', pattern: /^\/api\/v1\/projects$/, ipcMethod: 'project.list',
|
|
196
|
+
extractParams: () => ({}) },
|
|
197
|
+
|
|
198
|
+
// ─── Code ──────────────────────────────────────────────
|
|
199
|
+
{ method: 'POST', pattern: /^\/api\/v1\/code\/analyze$/, ipcMethod: 'code.analyze',
|
|
200
|
+
extractParams: (_m, _q, body) => body },
|
|
201
|
+
{ method: 'POST', pattern: /^\/api\/v1\/code\/find$/, ipcMethod: 'code.find',
|
|
202
|
+
extractParams: (_m, _q, body) => body },
|
|
203
|
+
{ method: 'POST', pattern: /^\/api\/v1\/code\/similarity$/, ipcMethod: 'code.similarity',
|
|
204
|
+
extractParams: (_m, _q, body) => body },
|
|
205
|
+
{ method: 'GET', pattern: /^\/api\/v1\/code\/modules$/, ipcMethod: 'code.modules',
|
|
206
|
+
extractParams: (_m, q) => ({
|
|
207
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
208
|
+
language: q.get('language') ?? undefined,
|
|
209
|
+
limit: q.get('limit') ? Number(q.get('limit')) : undefined,
|
|
210
|
+
}) },
|
|
211
|
+
{ method: 'GET', pattern: /^\/api\/v1\/code\/(\d+)$/, ipcMethod: 'code.get',
|
|
212
|
+
extractParams: (m) => ({ id: Number(m[1]) }) },
|
|
213
|
+
|
|
214
|
+
// ─── Prevention ────────────────────────────────────────
|
|
215
|
+
{ method: 'POST', pattern: /^\/api\/v1\/prevention\/check$/, ipcMethod: 'prevention.check',
|
|
216
|
+
extractParams: (_m, _q, body) => body },
|
|
217
|
+
{ method: 'POST', pattern: /^\/api\/v1\/prevention\/antipatterns$/, ipcMethod: 'prevention.antipatterns',
|
|
218
|
+
extractParams: (_m, _q, body) => body },
|
|
219
|
+
{ method: 'POST', pattern: /^\/api\/v1\/prevention\/code$/, ipcMethod: 'prevention.checkCode',
|
|
220
|
+
extractParams: (_m, _q, body) => body },
|
|
221
|
+
|
|
222
|
+
// ─── Synapses ─────────────────────────────────────────
|
|
223
|
+
{ method: 'GET', pattern: /^\/api\/v1\/synapses\/context\/(\d+)$/, ipcMethod: 'synapse.context',
|
|
224
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
225
|
+
{ method: 'POST', pattern: /^\/api\/v1\/synapses\/path$/, ipcMethod: 'synapse.path',
|
|
226
|
+
extractParams: (_m, _q, body) => body },
|
|
227
|
+
{ method: 'POST', pattern: /^\/api\/v1\/synapses\/related$/, ipcMethod: 'synapse.related',
|
|
228
|
+
extractParams: (_m, _q, body) => body },
|
|
229
|
+
{ method: 'GET', pattern: /^\/api\/v1\/synapses\/stats$/, ipcMethod: 'synapse.stats',
|
|
230
|
+
extractParams: () => ({}) },
|
|
231
|
+
|
|
232
|
+
// ─── Research ──────────────────────────────────────────
|
|
233
|
+
{ method: 'GET', pattern: /^\/api\/v1\/research\/insights$/, ipcMethod: 'research.insights',
|
|
234
|
+
extractParams: (_m, q) => ({
|
|
235
|
+
type: q.get('type') ?? undefined,
|
|
236
|
+
limit: q.get('limit') ? Number(q.get('limit')) : 20,
|
|
237
|
+
activeOnly: q.get('activeOnly') !== 'false',
|
|
238
|
+
}) },
|
|
239
|
+
{ method: 'POST', pattern: /^\/api\/v1\/research\/insights\/(\d+)\/rate$/, ipcMethod: 'insight.rate',
|
|
240
|
+
extractParams: (m, _q, body) => ({ id: Number(m[1]), ...(body as object) }) },
|
|
241
|
+
{ method: 'GET', pattern: /^\/api\/v1\/research\/suggest$/, ipcMethod: 'research.suggest',
|
|
242
|
+
extractParams: (_m, q) => ({
|
|
243
|
+
context: q.get('context') ?? '',
|
|
244
|
+
limit: 10,
|
|
245
|
+
activeOnly: true,
|
|
246
|
+
}) },
|
|
247
|
+
{ method: 'GET', pattern: /^\/api\/v1\/research\/trends$/, ipcMethod: 'research.trends',
|
|
248
|
+
extractParams: (_m, q) => ({
|
|
249
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
250
|
+
windowDays: q.get('windowDays') ? Number(q.get('windowDays')) : undefined,
|
|
251
|
+
}) },
|
|
252
|
+
|
|
253
|
+
// ─── Notifications ────────────────────────────────────
|
|
254
|
+
{ method: 'GET', pattern: /^\/api\/v1\/notifications$/, ipcMethod: 'notification.list',
|
|
255
|
+
extractParams: (_m, q) => ({
|
|
256
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
257
|
+
}) },
|
|
258
|
+
{ method: 'POST', pattern: /^\/api\/v1\/notifications\/(\d+)\/ack$/, ipcMethod: 'notification.ack',
|
|
259
|
+
extractParams: (m) => ({ id: Number(m[1]) }) },
|
|
260
|
+
|
|
261
|
+
// ─── Analytics ─────────────────────────────────────────
|
|
262
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/summary$/, ipcMethod: 'analytics.summary',
|
|
263
|
+
extractParams: (_m, q) => ({
|
|
264
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
265
|
+
}) },
|
|
266
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/network$/, ipcMethod: 'analytics.network',
|
|
267
|
+
extractParams: (_m, q) => ({
|
|
268
|
+
limit: q.get('limit') ? Number(q.get('limit')) : undefined,
|
|
269
|
+
}) },
|
|
270
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/health$/, ipcMethod: 'analytics.health',
|
|
271
|
+
extractParams: (_m, q) => ({
|
|
272
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
273
|
+
}) },
|
|
274
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/timeline$/, ipcMethod: 'analytics.timeline',
|
|
275
|
+
extractParams: (_m, q) => ({
|
|
276
|
+
projectId: q.get('projectId') ? Number(q.get('projectId')) : undefined,
|
|
277
|
+
days: q.get('days') ? Number(q.get('days')) : undefined,
|
|
278
|
+
}) },
|
|
279
|
+
{ method: 'GET', pattern: /^\/api\/v1\/analytics\/explain\/(\d+)$/, ipcMethod: 'analytics.explain',
|
|
280
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
281
|
+
|
|
282
|
+
// ─── Git ───────────────────────────────────────────────
|
|
283
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/context$/, ipcMethod: 'git.context',
|
|
284
|
+
extractParams: (_m, q) => ({ cwd: q.get('cwd') ?? undefined }) },
|
|
285
|
+
{ method: 'POST', pattern: /^\/api\/v1\/git\/link-error$/, ipcMethod: 'git.linkError',
|
|
286
|
+
extractParams: (_m, _q, body) => body },
|
|
287
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/errors\/(\d+)\/commits$/, ipcMethod: 'git.errorCommits',
|
|
288
|
+
extractParams: (m) => ({ errorId: Number(m[1]) }) },
|
|
289
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/commits\/([a-f0-9]+)\/errors$/, ipcMethod: 'git.commitErrors',
|
|
290
|
+
extractParams: (m) => ({ commitHash: m[1] }) },
|
|
291
|
+
{ method: 'GET', pattern: /^\/api\/v1\/git\/diff$/, ipcMethod: 'git.diff',
|
|
292
|
+
extractParams: (_m, q) => ({ cwd: q.get('cwd') ?? undefined }) },
|
|
293
|
+
|
|
294
|
+
// ─── Terminal ──────────────────────────────────────────
|
|
295
|
+
{ method: 'POST', pattern: /^\/api\/v1\/terminal\/register$/, ipcMethod: 'terminal.register',
|
|
296
|
+
extractParams: (_m, _q, body) => body },
|
|
297
|
+
{ method: 'POST', pattern: /^\/api\/v1\/terminal\/heartbeat$/, ipcMethod: 'terminal.heartbeat',
|
|
298
|
+
extractParams: (_m, _q, body) => body },
|
|
299
|
+
{ method: 'POST', pattern: /^\/api\/v1\/terminal\/disconnect$/, ipcMethod: 'terminal.disconnect',
|
|
300
|
+
extractParams: (_m, _q, body) => body },
|
|
301
|
+
|
|
302
|
+
// ─── Learning ──────────────────────────────────────────
|
|
303
|
+
{ method: 'POST', pattern: /^\/api\/v1\/learning\/run$/, ipcMethod: 'learning.run',
|
|
304
|
+
extractParams: () => ({}) },
|
|
305
|
+
];
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private json(res: http.ServerResponse, status: number, data: unknown): void {
|
|
309
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
310
|
+
res.end(JSON.stringify(data));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private readBody(req: http.IncomingMessage): Promise<string> {
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const chunks: Buffer[] = [];
|
|
316
|
+
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
317
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
318
|
+
req.on('error', reject);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
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,19 @@ 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 error service for hybrid search
|
|
124
|
+
services.error.setEmbeddingEngine(this.embeddingEngine);
|
|
125
|
+
logger.info('Embedding engine started (model will load in background)');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 9. Learning Engine
|
|
108
129
|
this.learningEngine = new LearningEngine(
|
|
109
130
|
config.learning, errorRepo, solutionRepo,
|
|
110
131
|
ruleRepo, antipatternRepo, synapseManager,
|
|
@@ -112,7 +133,7 @@ export class BrainCore {
|
|
|
112
133
|
this.learningEngine.start();
|
|
113
134
|
logger.info(`Learning engine started (interval: ${config.learning.intervalMs}ms)`);
|
|
114
135
|
|
|
115
|
-
//
|
|
136
|
+
// 10. Research Engine
|
|
116
137
|
this.researchEngine = new ResearchEngine(
|
|
117
138
|
config.research, errorRepo, solutionRepo, projectRepo,
|
|
118
139
|
codeModuleRepo, synapseRepo, insightRepo, synapseManager,
|
|
@@ -123,24 +144,42 @@ export class BrainCore {
|
|
|
123
144
|
// Expose learning engine to IPC
|
|
124
145
|
services.learning = this.learningEngine;
|
|
125
146
|
|
|
126
|
-
//
|
|
147
|
+
// 11. IPC Server
|
|
127
148
|
const router = new IpcRouter(services);
|
|
128
149
|
this.ipcServer = new IpcServer(router, config.ipc.pipeName);
|
|
129
150
|
this.ipcServer.start();
|
|
130
151
|
|
|
131
|
-
//
|
|
152
|
+
// 11a. REST API Server
|
|
153
|
+
if (config.api.enabled) {
|
|
154
|
+
this.apiServer = new ApiServer({
|
|
155
|
+
port: config.api.port,
|
|
156
|
+
router,
|
|
157
|
+
apiKey: config.api.apiKey,
|
|
158
|
+
});
|
|
159
|
+
this.apiServer.start();
|
|
160
|
+
logger.info(`REST API enabled on port ${config.api.port}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 11b. MCP HTTP Server (SSE transport for Cursor, Windsurf, Cline, Continue)
|
|
164
|
+
if (config.mcpHttp.enabled) {
|
|
165
|
+
this.mcpHttpServer = new McpHttpServer(config.mcpHttp.port, router);
|
|
166
|
+
this.mcpHttpServer.start();
|
|
167
|
+
logger.info(`MCP HTTP (SSE) enabled on port ${config.mcpHttp.port}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 12. Terminal cleanup timer
|
|
132
171
|
this.cleanupTimer = setInterval(() => {
|
|
133
172
|
services.terminal.cleanup();
|
|
134
173
|
}, 60_000);
|
|
135
174
|
|
|
136
|
-
//
|
|
175
|
+
// 13. Event listeners (synapse wiring)
|
|
137
176
|
this.setupEventListeners(services, synapseManager);
|
|
138
177
|
|
|
139
|
-
//
|
|
178
|
+
// 14. PID file
|
|
140
179
|
const pidPath = path.join(path.dirname(config.dbPath), 'brain.pid');
|
|
141
180
|
fs.writeFileSync(pidPath, String(process.pid));
|
|
142
181
|
|
|
143
|
-
//
|
|
182
|
+
// 15. Graceful shutdown
|
|
144
183
|
process.on('SIGINT', () => this.stop());
|
|
145
184
|
process.on('SIGTERM', () => this.stop());
|
|
146
185
|
|
|
@@ -157,7 +196,10 @@ export class BrainCore {
|
|
|
157
196
|
}
|
|
158
197
|
|
|
159
198
|
this.researchEngine?.stop();
|
|
199
|
+
this.embeddingEngine?.stop();
|
|
160
200
|
this.learningEngine?.stop();
|
|
201
|
+
this.mcpHttpServer?.stop();
|
|
202
|
+
this.apiServer?.stop();
|
|
161
203
|
this.ipcServer?.stop();
|
|
162
204
|
this.db?.close();
|
|
163
205
|
|
|
@@ -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...')}`);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { withIpc } from '../ipc-helper.js';
|
|
3
|
+
import { c, icons } from '../colors.js';
|
|
4
|
+
|
|
5
|
+
export function explainCommand(): Command {
|
|
6
|
+
return new Command('explain')
|
|
7
|
+
.description('Show everything Brain knows about an error')
|
|
8
|
+
.argument('<errorId>', 'Error ID to explain')
|
|
9
|
+
.action(async (errorId) => {
|
|
10
|
+
const id = parseInt(errorId, 10);
|
|
11
|
+
if (isNaN(id)) {
|
|
12
|
+
console.error(`${icons.error} Invalid error ID: ${errorId}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
await withIpc(async (client) => {
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
const result: any = await client.request('analytics.explain', { errorId: id });
|
|
19
|
+
|
|
20
|
+
if (!result.error) {
|
|
21
|
+
console.error(`${icons.error} Error #${id} not found.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const err = result.error;
|
|
26
|
+
console.log();
|
|
27
|
+
console.log(`${icons.brain} ${c.heading(`Error #${err.id} — ${err.type}`)}`);
|
|
28
|
+
console.log(`${c.dim('─'.repeat(60))}`);
|
|
29
|
+
console.log(` ${c.label('Message:')} ${err.message}`);
|
|
30
|
+
console.log(` ${c.label('File:')} ${err.file_path ?? 'unknown'}`);
|
|
31
|
+
console.log(` ${c.label('Context:')} ${err.context ?? 'none'}`);
|
|
32
|
+
console.log(` ${c.label('Seen:')} ${err.occurrence_count}x (first: ${err.first_seen}, last: ${err.last_seen})`);
|
|
33
|
+
console.log(` ${c.label('Resolved:')} ${err.resolved ? c.success('Yes') : c.error('No')}`);
|
|
34
|
+
console.log(` ${c.label('Synapses:')} ${result.synapseConnections} connections`);
|
|
35
|
+
|
|
36
|
+
// Error Chain
|
|
37
|
+
if (result.chain.parents.length > 0 || result.chain.children.length > 0) {
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(` ${c.heading('Error Chain:')}`);
|
|
40
|
+
for (const p of result.chain.parents) {
|
|
41
|
+
console.log(` ${c.dim('↑')} Caused by: #${p.id} ${p.type}: ${p.message.slice(0, 60)}`);
|
|
42
|
+
}
|
|
43
|
+
console.log(` ${c.info('→')} #${err.id} ${err.type}`);
|
|
44
|
+
for (const ch of result.chain.children) {
|
|
45
|
+
console.log(` ${c.dim('↓')} Led to: #${ch.id} ${ch.type}: ${ch.message.slice(0, 60)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Solutions
|
|
50
|
+
if (result.solutions.length > 0) {
|
|
51
|
+
console.log();
|
|
52
|
+
console.log(` ${c.heading('Solutions:')}`);
|
|
53
|
+
for (const s of result.solutions) {
|
|
54
|
+
const rate = `${Math.round(s.successRate * 100)}%`;
|
|
55
|
+
console.log(` ${icons.ok} #${s.id}: ${s.description.slice(0, 80)} (success: ${rate}, confidence: ${s.confidence.toFixed(2)})`);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(` ${c.dim('No solutions found.')}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Related Errors
|
|
63
|
+
if (result.relatedErrors.length > 0) {
|
|
64
|
+
console.log();
|
|
65
|
+
console.log(` ${c.heading('Related Errors:')}`);
|
|
66
|
+
for (const r of result.relatedErrors.slice(0, 5)) {
|
|
67
|
+
console.log(` ${c.dim('~')} #${r.id} ${r.type}: ${r.message.slice(0, 60)} (${Math.round(r.similarity * 100)}%)`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Rules
|
|
72
|
+
if (result.rules.length > 0) {
|
|
73
|
+
console.log();
|
|
74
|
+
console.log(` ${c.heading('Applicable Rules:')}`);
|
|
75
|
+
for (const r of result.rules) {
|
|
76
|
+
console.log(` ${icons.gear} #${r.id}: ${r.action} (confidence: ${r.confidence.toFixed(2)})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
package/src/code/analyzer.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface AnalysisResult {
|
|
|
10
10
|
isPure: boolean;
|
|
11
11
|
hasTypeAnnotations: boolean;
|
|
12
12
|
linesOfCode: number;
|
|
13
|
+
complexity: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const SIDE_EFFECT_PATTERNS = [
|
|
@@ -37,6 +38,7 @@ export function analyzeCode(source: string, language: string): AnalysisResult {
|
|
|
37
38
|
const isPure = checkPurity(source);
|
|
38
39
|
const typed = parser.hasTypeAnnotations(source);
|
|
39
40
|
const linesOfCode = source.split('\n').filter(l => l.trim().length > 0).length;
|
|
41
|
+
const complexity = computeCyclomaticComplexity(source, language);
|
|
40
42
|
|
|
41
43
|
return {
|
|
42
44
|
exports,
|
|
@@ -45,9 +47,47 @@ export function analyzeCode(source: string, language: string): AnalysisResult {
|
|
|
45
47
|
isPure,
|
|
46
48
|
hasTypeAnnotations: typed,
|
|
47
49
|
linesOfCode,
|
|
50
|
+
complexity,
|
|
48
51
|
};
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Computes cyclomatic complexity: counts decision points in the code.
|
|
56
|
+
* CC = 1 + number of decision points (if, else if, for, while, case, catch, &&, ||, ?:)
|
|
57
|
+
*/
|
|
58
|
+
export function computeCyclomaticComplexity(source: string, language: string): number {
|
|
59
|
+
// Remove comments and strings to avoid false positives
|
|
60
|
+
const cleaned = source
|
|
61
|
+
.replace(/\/\/.*$/gm, '') // single-line comments
|
|
62
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // multi-line comments
|
|
63
|
+
.replace(/#.*$/gm, '') // Python comments
|
|
64
|
+
.replace(/(["'`])(?:(?!\1|\\).|\\.)*\1/g, '""'); // strings
|
|
65
|
+
|
|
66
|
+
let complexity = 1; // Base complexity
|
|
67
|
+
|
|
68
|
+
// Language-agnostic decision point patterns
|
|
69
|
+
const patterns = [
|
|
70
|
+
/\bif\b/g,
|
|
71
|
+
/\belse\s+if\b/g,
|
|
72
|
+
/\belif\b/g,
|
|
73
|
+
/\bfor\b/g,
|
|
74
|
+
/\bwhile\b/g,
|
|
75
|
+
/\bcase\b/g,
|
|
76
|
+
/\bcatch\b/g,
|
|
77
|
+
/\bexcept\b/g,
|
|
78
|
+
/&&/g,
|
|
79
|
+
/\|\|/g,
|
|
80
|
+
/\?\s*[^:]/g, // ternary operator (not type annotations)
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (const pattern of patterns) {
|
|
84
|
+
const matches = cleaned.match(pattern);
|
|
85
|
+
if (matches) complexity += matches.length;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return complexity;
|
|
89
|
+
}
|
|
90
|
+
|
|
51
91
|
export function checkPurity(source: string): boolean {
|
|
52
92
|
return !SIDE_EFFECT_PATTERNS.some(p => source.includes(p));
|
|
53
93
|
}
|