@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.
Files changed (131) hide show
  1. package/README.md +225 -50
  2. package/dist/api/server.d.ts +23 -0
  3. package/dist/api/server.js +354 -0
  4. package/dist/api/server.js.map +1 -0
  5. package/dist/brain.d.ts +3 -0
  6. package/dist/brain.js +46 -8
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +37 -1
  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 +179 -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 +123 -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 +5 -0
  77. package/dist/services/code.service.js +91 -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 +395 -0
  98. package/src/brain.ts +51 -8
  99. package/src/cli/commands/dashboard.ts +38 -1
  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 +238 -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 +140 -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 +120 -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
@@ -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.2.0",
4
- "description": "Adaptive error memory and code intelligence system with Hebbian synapse network for Claude Code",
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. 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 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
- // 9. Research Engine
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
- // 10. IPC Server
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
- // 11. Terminal cleanup timer
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
- // 12. Event listeners (synapse wiring)
176
+ // 13. Event listeners (synapse wiring)
137
177
  this.setupEventListeners(services, synapseManager);
138
178
 
139
- // 13. PID file
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
- // 14. Graceful shutdown
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
- writeFileSync(outPath, html, 'utf-8');
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) {