@timmeck/brain 1.1.1 → 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 (139) 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/doctor.d.ts +2 -0
  11. package/dist/cli/commands/doctor.js +118 -0
  12. package/dist/cli/commands/doctor.js.map +1 -0
  13. package/dist/cli/commands/explain.d.ts +2 -0
  14. package/dist/cli/commands/explain.js +76 -0
  15. package/dist/cli/commands/explain.js.map +1 -0
  16. package/dist/cli/commands/projects.d.ts +2 -0
  17. package/dist/cli/commands/projects.js +36 -0
  18. package/dist/cli/commands/projects.js.map +1 -0
  19. package/dist/code/analyzer.d.ts +6 -0
  20. package/dist/code/analyzer.js +35 -0
  21. package/dist/code/analyzer.js.map +1 -1
  22. package/dist/code/matcher.d.ts +11 -1
  23. package/dist/code/matcher.js +49 -0
  24. package/dist/code/matcher.js.map +1 -1
  25. package/dist/code/scorer.d.ts +1 -0
  26. package/dist/code/scorer.js +15 -1
  27. package/dist/code/scorer.js.map +1 -1
  28. package/dist/config.js +31 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/dashboard/server.d.ts +15 -0
  31. package/dist/dashboard/server.js +124 -0
  32. package/dist/dashboard/server.js.map +1 -0
  33. package/dist/db/migrations/007_feedback.d.ts +2 -0
  34. package/dist/db/migrations/007_feedback.js +12 -0
  35. package/dist/db/migrations/007_feedback.js.map +1 -0
  36. package/dist/db/migrations/008_git_integration.d.ts +2 -0
  37. package/dist/db/migrations/008_git_integration.js +37 -0
  38. package/dist/db/migrations/008_git_integration.js.map +1 -0
  39. package/dist/db/migrations/009_embeddings.d.ts +2 -0
  40. package/dist/db/migrations/009_embeddings.js +7 -0
  41. package/dist/db/migrations/009_embeddings.js.map +1 -0
  42. package/dist/db/migrations/index.js +6 -0
  43. package/dist/db/migrations/index.js.map +1 -1
  44. package/dist/db/repositories/code-module.repository.d.ts +16 -0
  45. package/dist/db/repositories/code-module.repository.js +42 -0
  46. package/dist/db/repositories/code-module.repository.js.map +1 -1
  47. package/dist/db/repositories/error.repository.d.ts +5 -0
  48. package/dist/db/repositories/error.repository.js +27 -0
  49. package/dist/db/repositories/error.repository.js.map +1 -1
  50. package/dist/db/repositories/insight.repository.d.ts +2 -0
  51. package/dist/db/repositories/insight.repository.js +13 -0
  52. package/dist/db/repositories/insight.repository.js.map +1 -1
  53. package/dist/embeddings/engine.d.ts +42 -0
  54. package/dist/embeddings/engine.js +166 -0
  55. package/dist/embeddings/engine.js.map +1 -0
  56. package/dist/hooks/post-tool-use.js +2 -0
  57. package/dist/hooks/post-tool-use.js.map +1 -1
  58. package/dist/hooks/post-write.js +11 -0
  59. package/dist/hooks/post-write.js.map +1 -1
  60. package/dist/index.js +7 -1
  61. package/dist/index.js.map +1 -1
  62. package/dist/ipc/router.d.ts +2 -0
  63. package/dist/ipc/router.js +15 -0
  64. package/dist/ipc/router.js.map +1 -1
  65. package/dist/learning/confidence-scorer.d.ts +16 -0
  66. package/dist/learning/confidence-scorer.js +20 -0
  67. package/dist/learning/confidence-scorer.js.map +1 -1
  68. package/dist/learning/learning-engine.js +12 -5
  69. package/dist/learning/learning-engine.js.map +1 -1
  70. package/dist/matching/error-matcher.d.ts +9 -1
  71. package/dist/matching/error-matcher.js +50 -5
  72. package/dist/matching/error-matcher.js.map +1 -1
  73. package/dist/mcp/http-server.d.ts +14 -0
  74. package/dist/mcp/http-server.js +117 -0
  75. package/dist/mcp/http-server.js.map +1 -0
  76. package/dist/mcp/tools.d.ts +4 -0
  77. package/dist/mcp/tools.js +41 -14
  78. package/dist/mcp/tools.js.map +1 -1
  79. package/dist/services/analytics.service.d.ts +39 -0
  80. package/dist/services/analytics.service.js +111 -0
  81. package/dist/services/analytics.service.js.map +1 -1
  82. package/dist/services/code.service.d.ts +10 -0
  83. package/dist/services/code.service.js +73 -4
  84. package/dist/services/code.service.js.map +1 -1
  85. package/dist/services/error.service.d.ts +17 -1
  86. package/dist/services/error.service.js +90 -12
  87. package/dist/services/error.service.js.map +1 -1
  88. package/dist/services/git.service.d.ts +49 -0
  89. package/dist/services/git.service.js +112 -0
  90. package/dist/services/git.service.js.map +1 -0
  91. package/dist/services/prevention.service.d.ts +7 -0
  92. package/dist/services/prevention.service.js +38 -0
  93. package/dist/services/prevention.service.js.map +1 -1
  94. package/dist/services/research.service.d.ts +1 -0
  95. package/dist/services/research.service.js +4 -0
  96. package/dist/services/research.service.js.map +1 -1
  97. package/dist/services/solution.service.d.ts +10 -0
  98. package/dist/services/solution.service.js +48 -0
  99. package/dist/services/solution.service.js.map +1 -1
  100. package/dist/types/config.types.d.ts +21 -0
  101. package/dist/types/synapse.types.d.ts +1 -1
  102. package/package.json +8 -3
  103. package/src/api/server.ts +321 -0
  104. package/src/brain.ts +50 -8
  105. package/src/cli/commands/dashboard.ts +2 -0
  106. package/src/cli/commands/doctor.ts +118 -0
  107. package/src/cli/commands/explain.ts +83 -0
  108. package/src/cli/commands/projects.ts +42 -0
  109. package/src/code/analyzer.ts +40 -0
  110. package/src/code/matcher.ts +67 -2
  111. package/src/code/scorer.ts +13 -1
  112. package/src/config.ts +24 -0
  113. package/src/dashboard/server.ts +142 -0
  114. package/src/db/migrations/007_feedback.ts +13 -0
  115. package/src/db/migrations/008_git_integration.ts +38 -0
  116. package/src/db/migrations/009_embeddings.ts +8 -0
  117. package/src/db/migrations/index.ts +6 -0
  118. package/src/db/repositories/code-module.repository.ts +53 -0
  119. package/src/db/repositories/error.repository.ts +40 -0
  120. package/src/db/repositories/insight.repository.ts +21 -0
  121. package/src/embeddings/engine.ts +217 -0
  122. package/src/hooks/post-tool-use.ts +2 -0
  123. package/src/hooks/post-write.ts +12 -0
  124. package/src/index.ts +7 -1
  125. package/src/ipc/router.ts +19 -0
  126. package/src/learning/confidence-scorer.ts +33 -0
  127. package/src/learning/learning-engine.ts +13 -5
  128. package/src/matching/error-matcher.ts +55 -4
  129. package/src/mcp/http-server.ts +137 -0
  130. package/src/mcp/tools.ts +52 -14
  131. package/src/services/analytics.service.ts +136 -0
  132. package/src/services/code.service.ts +99 -4
  133. package/src/services/error.service.ts +114 -13
  134. package/src/services/git.service.ts +132 -0
  135. package/src/services/prevention.service.ts +40 -0
  136. package/src/services/research.service.ts +5 -0
  137. package/src/services/solution.service.ts +58 -0
  138. package/src/types/config.types.ts +24 -0
  139. package/src/types/synapse.types.ts +1 -0
@@ -0,0 +1,118 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import { getDataDir } from '../../utils/paths.js';
6
+ import { IpcClient } from '../../ipc/client.js';
7
+ import { getPipeName } from '../../utils/paths.js';
8
+ import { c, icons, header, divider } from '../colors.js';
9
+
10
+ function pass(label: string, detail?: string): void {
11
+ const extra = detail ? ` ${c.dim(detail)}` : '';
12
+ console.log(` ${c.green(icons.check)} ${label}${extra}`);
13
+ }
14
+
15
+ function fail(label: string, detail?: string): void {
16
+ const extra = detail ? ` ${c.dim(detail)}` : '';
17
+ console.log(` ${c.red(icons.cross)} ${label}${extra}`);
18
+ }
19
+
20
+ export function doctorCommand(): Command {
21
+ return new Command('doctor')
22
+ .description('Check Brain health: daemon, DB, MCP, hooks')
23
+ .action(async () => {
24
+ console.log(header('Brain Doctor', icons.brain));
25
+ console.log();
26
+
27
+ let allGood = true;
28
+
29
+ // 1. Daemon running?
30
+ const pidPath = path.join(getDataDir(), 'brain.pid');
31
+ let daemonRunning = false;
32
+ if (fs.existsSync(pidPath)) {
33
+ const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
34
+ try {
35
+ process.kill(pid, 0);
36
+ daemonRunning = true;
37
+ pass('Daemon running', `PID ${pid}`);
38
+ } catch {
39
+ fail('Daemon not running', 'stale PID file');
40
+ allGood = false;
41
+ }
42
+ } else {
43
+ fail('Daemon not running', 'no PID file');
44
+ allGood = false;
45
+ }
46
+
47
+ // 2. DB reachable? (only if daemon running)
48
+ if (daemonRunning) {
49
+ const client = new IpcClient(getPipeName(), 3000);
50
+ try {
51
+ await client.connect();
52
+ await client.request('analytics.summary', {});
53
+ pass('Database reachable');
54
+ } catch {
55
+ fail('Database not reachable');
56
+ allGood = false;
57
+ } finally {
58
+ client.disconnect();
59
+ }
60
+ } else {
61
+ fail('Database not reachable', 'daemon not running');
62
+ allGood = false;
63
+ }
64
+
65
+ // 3. MCP configured?
66
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
67
+ let mcpConfigured = false;
68
+ try {
69
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
70
+ if (settings.mcpServers?.brain || settings.mcpServers?.['brain-mcp']) {
71
+ mcpConfigured = true;
72
+ pass('MCP server configured');
73
+ } else {
74
+ fail('MCP server not configured', `edit ${settingsPath}`);
75
+ allGood = false;
76
+ }
77
+ } catch {
78
+ fail('MCP server not configured', 'settings.json not found');
79
+ allGood = false;
80
+ }
81
+
82
+ // 4. Hook active?
83
+ try {
84
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
85
+ const hooks = settings.hooks;
86
+ const hasPostToolUse = hooks?.PostToolUse?.some(
87
+ (h: { command?: string }) => h.command?.includes('brain') || h.command?.includes('post-tool-use'),
88
+ );
89
+ if (hasPostToolUse) {
90
+ pass('Auto-detect hook active');
91
+ } else {
92
+ fail('Auto-detect hook not configured', 'errors won\'t be tracked automatically');
93
+ allGood = false;
94
+ }
95
+ } catch {
96
+ fail('Auto-detect hook not configured');
97
+ allGood = false;
98
+ }
99
+
100
+ // 5. DB file size
101
+ const dbPath = path.join(getDataDir(), 'brain.db');
102
+ try {
103
+ const stat = fs.statSync(dbPath);
104
+ pass('Database file', `${(stat.size / 1024 / 1024).toFixed(1)} MB at ${dbPath}`);
105
+ } catch {
106
+ fail('Database file not found');
107
+ allGood = false;
108
+ }
109
+
110
+ console.log();
111
+ if (allGood) {
112
+ console.log(` ${icons.ok} ${c.success('All checks passed!')}`);
113
+ } else {
114
+ console.log(` ${icons.warn} ${c.warn('Some checks failed.')} Run ${c.cyan('brain start')} and check your MCP config.`);
115
+ }
116
+ console.log(`\n${divider()}`);
117
+ });
118
+ }
@@ -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
+ }
@@ -0,0 +1,42 @@
1
+ import { Command } from 'commander';
2
+ import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons, header, divider, table } from '../colors.js';
4
+
5
+ export function projectsCommand(): Command {
6
+ return new Command('projects')
7
+ .description('List all imported projects with stats')
8
+ .action(async () => {
9
+ await withIpc(async (client) => {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ const projects = await client.request('project.list', {}) as any[];
12
+
13
+ console.log(header('Projects', icons.module));
14
+
15
+ if (projects.length === 0) {
16
+ console.log(`\n ${c.dim('No projects imported yet.')} Use ${c.cyan('brain import <dir>')} to get started.`);
17
+ console.log(`\n${divider()}`);
18
+ return;
19
+ }
20
+
21
+ console.log();
22
+
23
+ const rows: string[][] = [
24
+ [c.dim(' #'), c.dim('Name'), c.dim('Language'), c.dim('Modules'), c.dim('Path')],
25
+ ];
26
+
27
+ for (const p of projects) {
28
+ rows.push([
29
+ c.dimmer(` ${p.id}`),
30
+ c.value(p.name),
31
+ p.language ? c.cyan(p.language) : c.dim('—'),
32
+ c.green(String(p.moduleCount)),
33
+ p.path ? c.dim(p.path) : c.dim('—'),
34
+ ]);
35
+ }
36
+
37
+ console.log(table(rows, [5, 24, 14, 9, 40]));
38
+ console.log(`\n ${c.label('Total:')} ${c.value(String(projects.length))} projects, ${c.green(String(projects.reduce((s: number, p: any) => s + p.moduleCount, 0)))} modules`);
39
+ console.log(`\n${divider()}`);
40
+ });
41
+ });
42
+ }
@@ -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
  }
@@ -1,12 +1,12 @@
1
1
  import type { CodeModuleRecord } from '../types/code.types.js';
2
2
  import { fingerprintCode } from './fingerprint.js';
3
3
  import { tokenize } from '../matching/tokenizer.js';
4
- import { cosineSimilarity, jaccardSimilarity } from '../matching/similarity.js';
4
+ import { cosineSimilarity } from '../matching/similarity.js';
5
5
 
6
6
  export interface CodeMatchResult {
7
7
  moduleId: number;
8
8
  score: number;
9
- matchType: 'exact' | 'structural' | 'semantic';
9
+ matchType: 'exact' | 'structural' | 'semantic' | 'vector';
10
10
  }
11
11
 
12
12
  export function findExactMatches(
@@ -62,3 +62,68 @@ export function findSemanticMatches(
62
62
  .filter(r => r.score >= threshold)
63
63
  .sort((a, b) => b.score - a.score);
64
64
  }
65
+
66
+ /**
67
+ * Find matches using pre-computed vector embeddings.
68
+ * Vector scores are computed externally (by the EmbeddingEngine) and passed in.
69
+ */
70
+ export function findVectorMatches(
71
+ vectorScores: Map<number, number>,
72
+ threshold: number = 0.5,
73
+ ): CodeMatchResult[] {
74
+ const results: CodeMatchResult[] = [];
75
+
76
+ for (const [moduleId, score] of vectorScores) {
77
+ if (score >= threshold) {
78
+ results.push({ moduleId, score, matchType: 'vector' });
79
+ }
80
+ }
81
+
82
+ return results.sort((a, b) => b.score - a.score);
83
+ }
84
+
85
+ /**
86
+ * Hybrid search: combine structural + semantic + vector matches,
87
+ * deduplicating and taking the highest score per module.
88
+ */
89
+ export function findHybridMatches(
90
+ source: string,
91
+ language: string,
92
+ description: string,
93
+ candidates: CodeModuleRecord[],
94
+ vectorScores?: Map<number, number>,
95
+ ): CodeMatchResult[] {
96
+ const scoreMap = new Map<number, CodeMatchResult>();
97
+
98
+ // Structural matches (highest priority)
99
+ for (const match of findStructuralMatches(source, language, candidates, 0.5)) {
100
+ const existing = scoreMap.get(match.moduleId);
101
+ if (!existing || match.score > existing.score) {
102
+ scoreMap.set(match.moduleId, match);
103
+ }
104
+ }
105
+
106
+ // Semantic matches
107
+ for (const match of findSemanticMatches(description, candidates, 0.3)) {
108
+ const existing = scoreMap.get(match.moduleId);
109
+ if (!existing || match.score > existing.score) {
110
+ scoreMap.set(match.moduleId, match);
111
+ }
112
+ }
113
+
114
+ // Vector matches (if available)
115
+ if (vectorScores && vectorScores.size > 0) {
116
+ for (const match of findVectorMatches(vectorScores, 0.4)) {
117
+ const existing = scoreMap.get(match.moduleId);
118
+ if (!existing) {
119
+ scoreMap.set(match.moduleId, match);
120
+ } else {
121
+ // Boost existing matches that also have high vector similarity
122
+ const vectorBoost = match.score * 0.15;
123
+ existing.score = Math.min(1.0, existing.score + vectorBoost);
124
+ }
125
+ }
126
+ }
127
+
128
+ return [...scoreMap.values()].sort((a, b) => b.score - a.score);
129
+ }
@@ -6,6 +6,7 @@ export interface CodeUnitForScoring {
6
6
  exports: ExportInfo[];
7
7
  internalDeps: string[];
8
8
  hasTypeAnnotations: boolean;
9
+ complexity?: number;
9
10
  }
10
11
 
11
12
  interface ReusabilitySignal {
@@ -17,7 +18,7 @@ interface ReusabilitySignal {
17
18
  const REUSABILITY_SIGNALS: ReusabilitySignal[] = [
18
19
  {
19
20
  name: 'single_responsibility',
20
- weight: 0.25,
21
+ weight: 0.20,
21
22
  check: (code) => {
22
23
  const count = code.exports.length;
23
24
  if (count === 0) return 0;
@@ -88,6 +89,17 @@ const REUSABILITY_SIGNALS: ReusabilitySignal[] = [
88
89
  return 0.1;
89
90
  },
90
91
  },
92
+ {
93
+ name: 'low_complexity',
94
+ weight: 0.10,
95
+ check: (code) => {
96
+ const cc = code.complexity ?? 1;
97
+ if (cc <= 5) return 1.0;
98
+ if (cc <= 10) return 0.7;
99
+ if (cc <= 20) return 0.4;
100
+ return 0.1;
101
+ },
102
+ },
91
103
  ];
92
104
 
93
105
  export const MODULE_THRESHOLD = 0.60;
package/src/config.ts CHANGED
@@ -10,6 +10,21 @@ const defaults: BrainConfig = {
10
10
  pipeName: getPipeName(),
11
11
  timeout: 5000,
12
12
  },
13
+ api: {
14
+ port: 7777,
15
+ enabled: true,
16
+ },
17
+ mcpHttp: {
18
+ port: 7778,
19
+ enabled: true,
20
+ },
21
+ embeddings: {
22
+ enabled: true,
23
+ modelName: 'Xenova/all-MiniLM-L6-v2',
24
+ cacheDir: path.join(getDataDir(), 'models'),
25
+ sweepIntervalMs: 300_000, // 5 minutes
26
+ batchSize: 50,
27
+ },
13
28
  learning: {
14
29
  intervalMs: 900_000,
15
30
  minOccurrences: 3,
@@ -27,6 +42,8 @@ const defaults: BrainConfig = {
27
42
  fingerprintFields: ['type', 'message', 'file_path'],
28
43
  similarityThreshold: 0.8,
29
44
  maxResults: 10,
45
+ crossProjectMatching: true,
46
+ crossProjectWeight: 0.7,
30
47
  },
31
48
  code: {
32
49
  supportedLanguages: ['typescript', 'javascript', 'python', 'rust', 'go'],
@@ -74,6 +91,13 @@ function applyEnvOverrides(config: BrainConfig): void {
74
91
  if (process.env['BRAIN_DB_PATH']) config.dbPath = process.env['BRAIN_DB_PATH'];
75
92
  if (process.env['BRAIN_LOG_LEVEL']) config.log.level = process.env['BRAIN_LOG_LEVEL'];
76
93
  if (process.env['BRAIN_PIPE_NAME']) config.ipc.pipeName = process.env['BRAIN_PIPE_NAME'];
94
+ if (process.env['BRAIN_API_PORT']) config.api.port = Number(process.env['BRAIN_API_PORT']);
95
+ if (process.env['BRAIN_API_ENABLED']) config.api.enabled = process.env['BRAIN_API_ENABLED'] !== 'false';
96
+ if (process.env['BRAIN_API_KEY']) config.api.apiKey = process.env['BRAIN_API_KEY'];
97
+ if (process.env['BRAIN_MCP_HTTP_PORT']) config.mcpHttp.port = Number(process.env['BRAIN_MCP_HTTP_PORT']);
98
+ if (process.env['BRAIN_MCP_HTTP_ENABLED']) config.mcpHttp.enabled = process.env['BRAIN_MCP_HTTP_ENABLED'] !== 'false';
99
+ if (process.env['BRAIN_EMBEDDINGS_ENABLED']) config.embeddings.enabled = process.env['BRAIN_EMBEDDINGS_ENABLED'] !== 'false';
100
+ if (process.env['BRAIN_EMBEDDINGS_MODEL']) config.embeddings.modelName = process.env['BRAIN_EMBEDDINGS_MODEL'];
77
101
  }
78
102
 
79
103
  function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
@@ -0,0 +1,142 @@
1
+ import http from 'node:http';
2
+ import { getEventBus } from '../utils/events.js';
3
+ import { getLogger } from '../utils/logger.js';
4
+
5
+ export interface DashboardServerOptions {
6
+ port: number;
7
+ getDashboardHtml: () => string;
8
+ getStats: () => unknown;
9
+ }
10
+
11
+ export class DashboardServer {
12
+ private server: http.Server | null = null;
13
+ private clients: Set<http.ServerResponse> = new Set();
14
+ private logger = getLogger();
15
+
16
+ constructor(private options: DashboardServerOptions) {}
17
+
18
+ start(): void {
19
+ const { port, getDashboardHtml, getStats } = this.options;
20
+ const bus = getEventBus();
21
+
22
+ this.server = http.createServer((req, res) => {
23
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
24
+
25
+ // CORS
26
+ res.setHeader('Access-Control-Allow-Origin', '*');
27
+ res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
28
+
29
+ if (req.method === 'OPTIONS') {
30
+ res.writeHead(204);
31
+ res.end();
32
+ return;
33
+ }
34
+
35
+ if (url.pathname === '/events') {
36
+ // SSE endpoint
37
+ res.writeHead(200, {
38
+ 'Content-Type': 'text/event-stream',
39
+ 'Cache-Control': 'no-cache',
40
+ 'Connection': 'keep-alive',
41
+ });
42
+ res.write('data: {"type":"connected"}\n\n');
43
+
44
+ this.clients.add(res);
45
+ req.on('close', () => this.clients.delete(res));
46
+ return;
47
+ }
48
+
49
+ if (url.pathname === '/api/stats') {
50
+ const stats = getStats();
51
+ res.writeHead(200, { 'Content-Type': 'application/json' });
52
+ res.end(JSON.stringify(stats));
53
+ return;
54
+ }
55
+
56
+ if (url.pathname === '/' || url.pathname === '/dashboard') {
57
+ const html = getDashboardHtml();
58
+ // Inject SSE script into the dashboard
59
+ const sseScript = `
60
+ <script>
61
+ (function(){
62
+ const evtSource = new EventSource('/events');
63
+ evtSource.onmessage = function(e) {
64
+ try {
65
+ const data = JSON.parse(e.data);
66
+ if (data.type === 'stats_update') {
67
+ // Update stat cards
68
+ document.querySelectorAll('.stat-number').forEach(el => {
69
+ const key = el.parentElement?.querySelector('.stat-label')?.textContent?.toLowerCase();
70
+ if (key && data.stats[key] !== undefined) {
71
+ el.textContent = Number(data.stats[key]).toLocaleString();
72
+ }
73
+ });
74
+ }
75
+ if (data.type === 'event') {
76
+ // Flash the activity dot
77
+ const dot = document.querySelector('.activity-dot');
78
+ if (dot) { dot.style.background = '#ff5577'; setTimeout(() => dot.style.background = '', 500); }
79
+ }
80
+ } catch {}
81
+ };
82
+ evtSource.onerror = function() { setTimeout(() => location.reload(), 5000); };
83
+ })();
84
+ </script>`;
85
+ const liveHtml = html.replace('</body>', sseScript + '</body>');
86
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
87
+ res.end(liveHtml);
88
+ return;
89
+ }
90
+
91
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
92
+ res.end('Not Found');
93
+ });
94
+
95
+ // Forward Brain events to SSE clients
96
+ const eventNames = [
97
+ 'error:reported', 'error:resolved', 'solution:applied',
98
+ 'solution:created', 'module:registered', 'module:updated',
99
+ 'synapse:created', 'synapse:strengthened',
100
+ 'insight:created', 'rule:learned',
101
+ ] as const;
102
+
103
+ for (const eventName of eventNames) {
104
+ bus.on(eventName, (data: unknown) => {
105
+ this.broadcast({ type: 'event', event: eventName, data });
106
+ });
107
+ }
108
+
109
+ // Periodic stats broadcast (every 30s)
110
+ setInterval(() => {
111
+ if (this.clients.size > 0) {
112
+ const stats = getStats();
113
+ this.broadcast({ type: 'stats_update', stats });
114
+ }
115
+ }, 30_000);
116
+
117
+ this.server.listen(port, () => {
118
+ this.logger.info(`Dashboard server started on http://localhost:${port}`);
119
+ });
120
+ }
121
+
122
+ stop(): void {
123
+ for (const client of this.clients) {
124
+ client.end();
125
+ }
126
+ this.clients.clear();
127
+ this.server?.close();
128
+ this.server = null;
129
+ this.logger.info('Dashboard server stopped');
130
+ }
131
+
132
+ private broadcast(data: unknown): void {
133
+ const msg = `data: ${JSON.stringify(data)}\n\n`;
134
+ for (const client of this.clients) {
135
+ try {
136
+ client.write(msg);
137
+ } catch {
138
+ this.clients.delete(client);
139
+ }
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,13 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export function up(db: Database.Database): void {
4
+ db.exec(`
5
+ ALTER TABLE insights ADD COLUMN rating INTEGER DEFAULT NULL;
6
+ ALTER TABLE insights ADD COLUMN rating_comment TEXT DEFAULT NULL;
7
+ ALTER TABLE insights ADD COLUMN rated_at TEXT DEFAULT NULL;
8
+
9
+ ALTER TABLE rules ADD COLUMN rating INTEGER DEFAULT NULL;
10
+ ALTER TABLE rules ADD COLUMN rating_comment TEXT DEFAULT NULL;
11
+ ALTER TABLE rules ADD COLUMN rated_at TEXT DEFAULT NULL;
12
+ `);
13
+ }
@@ -0,0 +1,38 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export function up(db: Database.Database): void {
4
+ db.exec(`
5
+ CREATE TABLE IF NOT EXISTS git_commits (
6
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
7
+ project_id INTEGER NOT NULL,
8
+ commit_hash TEXT NOT NULL,
9
+ message TEXT NOT NULL,
10
+ author TEXT,
11
+ timestamp TEXT NOT NULL,
12
+ files_changed INTEGER NOT NULL DEFAULT 0,
13
+ insertions INTEGER NOT NULL DEFAULT 0,
14
+ deletions INTEGER NOT NULL DEFAULT 0,
15
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
16
+ FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
17
+ UNIQUE(project_id, commit_hash)
18
+ );
19
+
20
+ CREATE TABLE IF NOT EXISTS error_commits (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ error_id INTEGER NOT NULL,
23
+ commit_hash TEXT NOT NULL,
24
+ relationship TEXT NOT NULL DEFAULT 'introduced_by',
25
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26
+ FOREIGN KEY (error_id) REFERENCES errors(id) ON DELETE CASCADE,
27
+ UNIQUE(error_id, commit_hash, relationship)
28
+ );
29
+
30
+ ALTER TABLE errors ADD COLUMN git_diff TEXT DEFAULT NULL;
31
+ ALTER TABLE errors ADD COLUMN git_branch TEXT DEFAULT NULL;
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_git_commits_project ON git_commits(project_id);
34
+ CREATE INDEX IF NOT EXISTS idx_git_commits_hash ON git_commits(commit_hash);
35
+ CREATE INDEX IF NOT EXISTS idx_error_commits_error ON error_commits(error_id);
36
+ CREATE INDEX IF NOT EXISTS idx_error_commits_hash ON error_commits(commit_hash);
37
+ `);
38
+ }
@@ -0,0 +1,8 @@
1
+ import type Database from 'better-sqlite3';
2
+
3
+ export function up(db: Database.Database): void {
4
+ db.exec(`
5
+ ALTER TABLE errors ADD COLUMN embedding BLOB DEFAULT NULL;
6
+ ALTER TABLE code_modules ADD COLUMN embedding BLOB DEFAULT NULL;
7
+ `);
8
+ }