@timmeck/brain 1.8.0 → 1.8.2

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 (177) hide show
  1. package/BRAIN_PLAN.md +3324 -3324
  2. package/LICENSE +21 -21
  3. package/dist/api/server.d.ts +4 -0
  4. package/dist/api/server.js +73 -0
  5. package/dist/api/server.js.map +1 -1
  6. package/dist/brain.js +2 -1
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +606 -572
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/dashboard/server.js +25 -25
  11. package/dist/db/migrations/001_core_schema.js +115 -115
  12. package/dist/db/migrations/002_learning_schema.js +33 -33
  13. package/dist/db/migrations/003_code_schema.js +48 -48
  14. package/dist/db/migrations/004_synapses_schema.js +52 -52
  15. package/dist/db/migrations/005_fts_indexes.js +73 -73
  16. package/dist/db/migrations/007_feedback.js +8 -8
  17. package/dist/db/migrations/008_git_integration.js +33 -33
  18. package/dist/db/migrations/009_embeddings.js +3 -3
  19. package/dist/db/repositories/antipattern.repository.js +3 -3
  20. package/dist/db/repositories/code-module.repository.js +32 -32
  21. package/dist/db/repositories/notification.repository.js +3 -3
  22. package/dist/db/repositories/project.repository.js +21 -21
  23. package/dist/db/repositories/rule.repository.js +24 -24
  24. package/dist/db/repositories/solution.repository.js +50 -50
  25. package/dist/db/repositories/synapse.repository.js +18 -18
  26. package/dist/db/repositories/terminal.repository.js +24 -24
  27. package/dist/embeddings/engine.d.ts +2 -2
  28. package/dist/embeddings/engine.js +17 -4
  29. package/dist/embeddings/engine.js.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/ipc/server.d.ts +8 -0
  32. package/dist/ipc/server.js +67 -1
  33. package/dist/ipc/server.js.map +1 -1
  34. package/dist/matching/error-matcher.js +5 -5
  35. package/dist/matching/fingerprint.js +6 -1
  36. package/dist/matching/fingerprint.js.map +1 -1
  37. package/dist/mcp/http-server.js +8 -2
  38. package/dist/mcp/http-server.js.map +1 -1
  39. package/dist/services/code.service.d.ts +3 -0
  40. package/dist/services/code.service.js +33 -4
  41. package/dist/services/code.service.js.map +1 -1
  42. package/dist/services/error.service.js +4 -3
  43. package/dist/services/error.service.js.map +1 -1
  44. package/dist/services/git.service.js +14 -14
  45. package/package.json +49 -49
  46. package/src/api/server.ts +395 -321
  47. package/src/brain.ts +266 -265
  48. package/src/cli/colors.ts +116 -116
  49. package/src/cli/commands/config.ts +169 -169
  50. package/src/cli/commands/dashboard.ts +755 -720
  51. package/src/cli/commands/doctor.ts +118 -118
  52. package/src/cli/commands/explain.ts +83 -83
  53. package/src/cli/commands/export.ts +31 -31
  54. package/src/cli/commands/import.ts +199 -199
  55. package/src/cli/commands/insights.ts +65 -65
  56. package/src/cli/commands/learn.ts +24 -24
  57. package/src/cli/commands/modules.ts +53 -53
  58. package/src/cli/commands/network.ts +67 -67
  59. package/src/cli/commands/projects.ts +42 -42
  60. package/src/cli/commands/query.ts +120 -120
  61. package/src/cli/commands/start.ts +62 -62
  62. package/src/cli/commands/status.ts +75 -75
  63. package/src/cli/commands/stop.ts +34 -34
  64. package/src/cli/ipc-helper.ts +22 -22
  65. package/src/cli/update-check.ts +63 -63
  66. package/src/code/fingerprint.ts +87 -87
  67. package/src/code/parsers/generic.ts +29 -29
  68. package/src/code/parsers/python.ts +54 -54
  69. package/src/code/parsers/typescript.ts +65 -65
  70. package/src/code/registry.ts +60 -60
  71. package/src/dashboard/server.ts +142 -142
  72. package/src/db/connection.ts +22 -22
  73. package/src/db/migrations/001_core_schema.ts +120 -120
  74. package/src/db/migrations/002_learning_schema.ts +38 -38
  75. package/src/db/migrations/003_code_schema.ts +53 -53
  76. package/src/db/migrations/004_synapses_schema.ts +57 -57
  77. package/src/db/migrations/005_fts_indexes.ts +78 -78
  78. package/src/db/migrations/006_synapses_phase3.ts +17 -17
  79. package/src/db/migrations/007_feedback.ts +13 -13
  80. package/src/db/migrations/008_git_integration.ts +38 -38
  81. package/src/db/migrations/009_embeddings.ts +8 -8
  82. package/src/db/repositories/antipattern.repository.ts +66 -66
  83. package/src/db/repositories/code-module.repository.ts +142 -142
  84. package/src/db/repositories/notification.repository.ts +66 -66
  85. package/src/db/repositories/project.repository.ts +93 -93
  86. package/src/db/repositories/rule.repository.ts +108 -108
  87. package/src/db/repositories/solution.repository.ts +154 -154
  88. package/src/db/repositories/synapse.repository.ts +153 -153
  89. package/src/db/repositories/terminal.repository.ts +101 -101
  90. package/src/embeddings/engine.ts +238 -217
  91. package/src/index.ts +63 -63
  92. package/src/ipc/client.ts +118 -118
  93. package/src/ipc/protocol.ts +35 -35
  94. package/src/ipc/router.ts +133 -133
  95. package/src/ipc/server.ts +176 -110
  96. package/src/learning/decay.ts +46 -46
  97. package/src/learning/pattern-extractor.ts +90 -90
  98. package/src/learning/rule-generator.ts +74 -74
  99. package/src/matching/error-matcher.ts +5 -5
  100. package/src/matching/fingerprint.ts +34 -29
  101. package/src/matching/similarity.ts +61 -61
  102. package/src/matching/tfidf.ts +74 -74
  103. package/src/matching/tokenizer.ts +41 -41
  104. package/src/mcp/auto-detect.ts +93 -93
  105. package/src/mcp/http-server.ts +140 -137
  106. package/src/mcp/server.ts +73 -73
  107. package/src/parsing/error-parser.ts +28 -28
  108. package/src/parsing/parsers/compiler.ts +93 -93
  109. package/src/parsing/parsers/generic.ts +28 -28
  110. package/src/parsing/parsers/go.ts +97 -97
  111. package/src/parsing/parsers/node.ts +69 -69
  112. package/src/parsing/parsers/python.ts +62 -62
  113. package/src/parsing/parsers/rust.ts +50 -50
  114. package/src/parsing/parsers/shell.ts +42 -42
  115. package/src/parsing/types.ts +47 -47
  116. package/src/research/gap-analyzer.ts +135 -135
  117. package/src/research/insight-generator.ts +123 -123
  118. package/src/research/research-engine.ts +116 -116
  119. package/src/research/synergy-detector.ts +126 -126
  120. package/src/research/template-extractor.ts +130 -130
  121. package/src/research/trend-analyzer.ts +127 -127
  122. package/src/services/code.service.ts +271 -238
  123. package/src/services/error.service.ts +4 -3
  124. package/src/services/git.service.ts +132 -132
  125. package/src/services/notification.service.ts +41 -41
  126. package/src/services/synapse.service.ts +59 -59
  127. package/src/services/terminal.service.ts +81 -81
  128. package/src/synapses/activation.ts +80 -80
  129. package/src/synapses/decay.ts +38 -38
  130. package/src/synapses/hebbian.ts +69 -69
  131. package/src/synapses/pathfinder.ts +81 -81
  132. package/src/synapses/synapse-manager.ts +109 -109
  133. package/src/types/code.types.ts +52 -52
  134. package/src/types/error.types.ts +67 -67
  135. package/src/types/ipc.types.ts +8 -8
  136. package/src/types/mcp.types.ts +53 -53
  137. package/src/types/research.types.ts +28 -28
  138. package/src/types/solution.types.ts +30 -30
  139. package/src/utils/events.ts +45 -45
  140. package/src/utils/hash.ts +5 -5
  141. package/src/utils/logger.ts +48 -48
  142. package/src/utils/paths.ts +19 -19
  143. package/tests/e2e/test_code_intelligence.py +1015 -0
  144. package/tests/e2e/test_error_memory.py +451 -0
  145. package/tests/e2e/test_full_integration.py +534 -0
  146. package/tests/fixtures/code-modules/modules.ts +83 -83
  147. package/tests/fixtures/errors/go.ts +9 -9
  148. package/tests/fixtures/errors/node.ts +24 -24
  149. package/tests/fixtures/errors/python.ts +21 -21
  150. package/tests/fixtures/errors/rust.ts +25 -25
  151. package/tests/fixtures/errors/shell.ts +15 -15
  152. package/tests/fixtures/solutions/solutions.ts +27 -27
  153. package/tests/helpers/setup-db.ts +52 -52
  154. package/tests/integration/code-flow.test.ts +86 -86
  155. package/tests/integration/error-flow.test.ts +83 -83
  156. package/tests/integration/ipc-flow.test.ts +166 -166
  157. package/tests/integration/learning-cycle.test.ts +82 -82
  158. package/tests/integration/synapse-flow.test.ts +117 -117
  159. package/tests/unit/code/analyzer.test.ts +58 -58
  160. package/tests/unit/code/fingerprint.test.ts +51 -51
  161. package/tests/unit/code/scorer.test.ts +55 -55
  162. package/tests/unit/learning/confidence-scorer.test.ts +60 -60
  163. package/tests/unit/learning/decay.test.ts +45 -45
  164. package/tests/unit/learning/pattern-extractor.test.ts +50 -50
  165. package/tests/unit/matching/error-matcher.test.ts +69 -69
  166. package/tests/unit/matching/fingerprint.test.ts +47 -47
  167. package/tests/unit/matching/similarity.test.ts +65 -65
  168. package/tests/unit/matching/tfidf.test.ts +71 -71
  169. package/tests/unit/matching/tokenizer.test.ts +83 -83
  170. package/tests/unit/parsing/parsers.test.ts +113 -113
  171. package/tests/unit/research/gap-analyzer.test.ts +45 -45
  172. package/tests/unit/research/trend-analyzer.test.ts +45 -45
  173. package/tests/unit/synapses/activation.test.ts +80 -80
  174. package/tests/unit/synapses/decay.test.ts +27 -27
  175. package/tests/unit/synapses/hebbian.test.ts +96 -96
  176. package/tests/unit/synapses/pathfinder.test.ts +72 -72
  177. package/tsconfig.json +18 -18
package/src/api/server.ts CHANGED
@@ -1,321 +1,395 @@
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
- }
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
+ }