@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.
Files changed (131) hide show
  1. package/README.md +225 -50
  2. package/dist/api/server.d.ts +19 -0
  3. package/dist/api/server.js +281 -0
  4. package/dist/api/server.js.map +1 -0
  5. package/dist/brain.d.ts +3 -0
  6. package/dist/brain.js +45 -8
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +2 -0
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/cli/commands/explain.d.ts +2 -0
  11. package/dist/cli/commands/explain.js +76 -0
  12. package/dist/cli/commands/explain.js.map +1 -0
  13. package/dist/code/analyzer.d.ts +6 -0
  14. package/dist/code/analyzer.js +35 -0
  15. package/dist/code/analyzer.js.map +1 -1
  16. package/dist/code/matcher.d.ts +11 -1
  17. package/dist/code/matcher.js +49 -0
  18. package/dist/code/matcher.js.map +1 -1
  19. package/dist/code/scorer.d.ts +1 -0
  20. package/dist/code/scorer.js +15 -1
  21. package/dist/code/scorer.js.map +1 -1
  22. package/dist/config.js +31 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/dashboard/server.d.ts +15 -0
  25. package/dist/dashboard/server.js +124 -0
  26. package/dist/dashboard/server.js.map +1 -0
  27. package/dist/db/migrations/007_feedback.d.ts +2 -0
  28. package/dist/db/migrations/007_feedback.js +12 -0
  29. package/dist/db/migrations/007_feedback.js.map +1 -0
  30. package/dist/db/migrations/008_git_integration.d.ts +2 -0
  31. package/dist/db/migrations/008_git_integration.js +37 -0
  32. package/dist/db/migrations/008_git_integration.js.map +1 -0
  33. package/dist/db/migrations/009_embeddings.d.ts +2 -0
  34. package/dist/db/migrations/009_embeddings.js +7 -0
  35. package/dist/db/migrations/009_embeddings.js.map +1 -0
  36. package/dist/db/migrations/index.js +6 -0
  37. package/dist/db/migrations/index.js.map +1 -1
  38. package/dist/db/repositories/code-module.repository.d.ts +16 -0
  39. package/dist/db/repositories/code-module.repository.js +42 -0
  40. package/dist/db/repositories/code-module.repository.js.map +1 -1
  41. package/dist/db/repositories/error.repository.d.ts +5 -0
  42. package/dist/db/repositories/error.repository.js +27 -0
  43. package/dist/db/repositories/error.repository.js.map +1 -1
  44. package/dist/db/repositories/insight.repository.d.ts +2 -0
  45. package/dist/db/repositories/insight.repository.js +13 -0
  46. package/dist/db/repositories/insight.repository.js.map +1 -1
  47. package/dist/embeddings/engine.d.ts +42 -0
  48. package/dist/embeddings/engine.js +166 -0
  49. package/dist/embeddings/engine.js.map +1 -0
  50. package/dist/hooks/post-tool-use.js +2 -0
  51. package/dist/hooks/post-tool-use.js.map +1 -1
  52. package/dist/hooks/post-write.js +11 -0
  53. package/dist/hooks/post-write.js.map +1 -1
  54. package/dist/index.js +3 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/ipc/router.d.ts +2 -0
  57. package/dist/ipc/router.js +13 -0
  58. package/dist/ipc/router.js.map +1 -1
  59. package/dist/learning/confidence-scorer.d.ts +16 -0
  60. package/dist/learning/confidence-scorer.js +20 -0
  61. package/dist/learning/confidence-scorer.js.map +1 -1
  62. package/dist/learning/learning-engine.js +12 -5
  63. package/dist/learning/learning-engine.js.map +1 -1
  64. package/dist/matching/error-matcher.d.ts +9 -1
  65. package/dist/matching/error-matcher.js +50 -5
  66. package/dist/matching/error-matcher.js.map +1 -1
  67. package/dist/mcp/http-server.d.ts +14 -0
  68. package/dist/mcp/http-server.js +117 -0
  69. package/dist/mcp/http-server.js.map +1 -0
  70. package/dist/mcp/tools.d.ts +4 -0
  71. package/dist/mcp/tools.js +41 -14
  72. package/dist/mcp/tools.js.map +1 -1
  73. package/dist/services/analytics.service.d.ts +39 -0
  74. package/dist/services/analytics.service.js +111 -0
  75. package/dist/services/analytics.service.js.map +1 -1
  76. package/dist/services/code.service.d.ts +2 -0
  77. package/dist/services/code.service.js +62 -4
  78. package/dist/services/code.service.js.map +1 -1
  79. package/dist/services/error.service.d.ts +17 -1
  80. package/dist/services/error.service.js +90 -12
  81. package/dist/services/error.service.js.map +1 -1
  82. package/dist/services/git.service.d.ts +49 -0
  83. package/dist/services/git.service.js +112 -0
  84. package/dist/services/git.service.js.map +1 -0
  85. package/dist/services/prevention.service.d.ts +7 -0
  86. package/dist/services/prevention.service.js +38 -0
  87. package/dist/services/prevention.service.js.map +1 -1
  88. package/dist/services/research.service.d.ts +1 -0
  89. package/dist/services/research.service.js +4 -0
  90. package/dist/services/research.service.js.map +1 -1
  91. package/dist/services/solution.service.d.ts +10 -0
  92. package/dist/services/solution.service.js +48 -0
  93. package/dist/services/solution.service.js.map +1 -1
  94. package/dist/types/config.types.d.ts +21 -0
  95. package/dist/types/synapse.types.d.ts +1 -1
  96. package/package.json +8 -3
  97. package/src/api/server.ts +321 -0
  98. package/src/brain.ts +50 -8
  99. package/src/cli/commands/dashboard.ts +2 -0
  100. package/src/cli/commands/explain.ts +83 -0
  101. package/src/code/analyzer.ts +40 -0
  102. package/src/code/matcher.ts +67 -2
  103. package/src/code/scorer.ts +13 -1
  104. package/src/config.ts +24 -0
  105. package/src/dashboard/server.ts +142 -0
  106. package/src/db/migrations/007_feedback.ts +13 -0
  107. package/src/db/migrations/008_git_integration.ts +38 -0
  108. package/src/db/migrations/009_embeddings.ts +8 -0
  109. package/src/db/migrations/index.ts +6 -0
  110. package/src/db/repositories/code-module.repository.ts +53 -0
  111. package/src/db/repositories/error.repository.ts +40 -0
  112. package/src/db/repositories/insight.repository.ts +21 -0
  113. package/src/embeddings/engine.ts +217 -0
  114. package/src/hooks/post-tool-use.ts +2 -0
  115. package/src/hooks/post-write.ts +12 -0
  116. package/src/index.ts +3 -1
  117. package/src/ipc/router.ts +16 -0
  118. package/src/learning/confidence-scorer.ts +33 -0
  119. package/src/learning/learning-engine.ts +13 -5
  120. package/src/matching/error-matcher.ts +55 -4
  121. package/src/mcp/http-server.ts +137 -0
  122. package/src/mcp/tools.ts +52 -14
  123. package/src/services/analytics.service.ts +136 -0
  124. package/src/services/code.service.ts +87 -4
  125. package/src/services/error.service.ts +114 -13
  126. package/src/services/git.service.ts +132 -0
  127. package/src/services/prevention.service.ts +40 -0
  128. package/src/services/research.service.ts +5 -0
  129. package/src/services/solution.service.ts +58 -0
  130. package/src/types/config.types.ts +24 -0
  131. 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. Learning Engine
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
- // 9. Research Engine
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
- // 10. IPC Server
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
- // 11. Terminal cleanup timer
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
- // 12. Event listeners (synapse wiring)
175
+ // 13. Event listeners (synapse wiring)
137
176
  this.setupEventListeners(services, synapseManager);
138
177
 
139
- // 13. PID file
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
- // 14. Graceful shutdown
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
+ }
@@ -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
  }