@timmeck/brain 1.0.0 → 1.1.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 (202) hide show
  1. package/BRAIN_PLAN.md +3324 -3324
  2. package/LICENSE +21 -21
  3. package/README.md +194 -188
  4. package/dist/brain.js +2 -0
  5. package/dist/brain.js.map +1 -1
  6. package/dist/cli/colors.d.ts +50 -0
  7. package/dist/cli/colors.js +106 -0
  8. package/dist/cli/colors.js.map +1 -0
  9. package/dist/cli/commands/config.d.ts +2 -0
  10. package/dist/cli/commands/config.js +165 -0
  11. package/dist/cli/commands/config.js.map +1 -0
  12. package/dist/cli/commands/dashboard.js +222 -8
  13. package/dist/cli/commands/dashboard.js.map +1 -1
  14. package/dist/cli/commands/export.js +3 -0
  15. package/dist/cli/commands/export.js.map +1 -1
  16. package/dist/cli/commands/import.js +24 -15
  17. package/dist/cli/commands/import.js.map +1 -1
  18. package/dist/cli/commands/insights.js +33 -6
  19. package/dist/cli/commands/insights.js.map +1 -1
  20. package/dist/cli/commands/learn.d.ts +2 -0
  21. package/dist/cli/commands/learn.js +22 -0
  22. package/dist/cli/commands/learn.js.map +1 -0
  23. package/dist/cli/commands/modules.js +25 -6
  24. package/dist/cli/commands/modules.js.map +1 -1
  25. package/dist/cli/commands/network.js +15 -9
  26. package/dist/cli/commands/network.js.map +1 -1
  27. package/dist/cli/commands/query.js +92 -25
  28. package/dist/cli/commands/query.js.map +1 -1
  29. package/dist/cli/commands/start.js +8 -5
  30. package/dist/cli/commands/start.js.map +1 -1
  31. package/dist/cli/commands/status.js +21 -16
  32. package/dist/cli/commands/status.js.map +1 -1
  33. package/dist/cli/commands/stop.js +5 -4
  34. package/dist/cli/commands/stop.js.map +1 -1
  35. package/dist/cli/ipc-helper.js +4 -3
  36. package/dist/cli/ipc-helper.js.map +1 -1
  37. package/dist/cli/update-check.d.ts +2 -0
  38. package/dist/cli/update-check.js +58 -0
  39. package/dist/cli/update-check.js.map +1 -0
  40. package/dist/db/migrations/001_core_schema.js +115 -115
  41. package/dist/db/migrations/002_learning_schema.js +33 -33
  42. package/dist/db/migrations/003_code_schema.js +48 -48
  43. package/dist/db/migrations/004_synapses_schema.js +52 -52
  44. package/dist/db/migrations/005_fts_indexes.js +73 -73
  45. package/dist/db/migrations/index.js +6 -6
  46. package/dist/db/repositories/antipattern.repository.js +3 -3
  47. package/dist/db/repositories/code-module.repository.d.ts +1 -0
  48. package/dist/db/repositories/code-module.repository.js +8 -0
  49. package/dist/db/repositories/code-module.repository.js.map +1 -1
  50. package/dist/db/repositories/error.repository.js +46 -46
  51. package/dist/db/repositories/insight.repository.js +3 -3
  52. package/dist/db/repositories/notification.repository.js +3 -3
  53. package/dist/db/repositories/project.repository.js +21 -21
  54. package/dist/db/repositories/rule.repository.js +24 -24
  55. package/dist/db/repositories/solution.repository.js +50 -50
  56. package/dist/db/repositories/synapse.repository.js +18 -18
  57. package/dist/db/repositories/terminal.repository.js +24 -24
  58. package/dist/index.js +4 -0
  59. package/dist/index.js.map +1 -1
  60. package/dist/ipc/router.d.ts +2 -0
  61. package/dist/ipc/router.js +7 -1
  62. package/dist/ipc/router.js.map +1 -1
  63. package/dist/services/code.service.d.ts +1 -1
  64. package/dist/services/code.service.js +5 -2
  65. package/dist/services/code.service.js.map +1 -1
  66. package/package.json +5 -4
  67. package/src/brain.ts +3 -0
  68. package/src/cli/colors.ts +116 -0
  69. package/src/cli/commands/config.ts +169 -0
  70. package/src/cli/commands/dashboard.ts +231 -8
  71. package/src/cli/commands/export.ts +4 -0
  72. package/src/cli/commands/import.ts +24 -15
  73. package/src/cli/commands/insights.ts +37 -5
  74. package/src/cli/commands/learn.ts +24 -0
  75. package/src/cli/commands/modules.ts +28 -5
  76. package/src/cli/commands/network.ts +15 -9
  77. package/src/cli/commands/query.ts +103 -26
  78. package/src/cli/commands/start.ts +8 -5
  79. package/src/cli/commands/status.ts +22 -16
  80. package/src/cli/commands/stop.ts +5 -4
  81. package/src/cli/ipc-helper.ts +4 -3
  82. package/src/cli/update-check.ts +63 -0
  83. package/src/code/analyzer.ts +77 -77
  84. package/src/code/fingerprint.ts +87 -87
  85. package/src/code/matcher.ts +64 -64
  86. package/src/code/parsers/generic.ts +29 -29
  87. package/src/code/parsers/python.ts +54 -54
  88. package/src/code/parsers/typescript.ts +65 -65
  89. package/src/code/registry.ts +60 -60
  90. package/src/code/scorer.ts +108 -108
  91. package/src/config.ts +111 -111
  92. package/src/db/connection.ts +22 -22
  93. package/src/db/migrations/001_core_schema.ts +120 -120
  94. package/src/db/migrations/002_learning_schema.ts +38 -38
  95. package/src/db/migrations/003_code_schema.ts +53 -53
  96. package/src/db/migrations/004_synapses_schema.ts +57 -57
  97. package/src/db/migrations/005_fts_indexes.ts +78 -78
  98. package/src/db/migrations/006_synapses_phase3.ts +17 -17
  99. package/src/db/migrations/index.ts +64 -64
  100. package/src/db/repositories/antipattern.repository.ts +66 -66
  101. package/src/db/repositories/code-module.repository.ts +9 -0
  102. package/src/db/repositories/error.repository.ts +149 -149
  103. package/src/db/repositories/insight.repository.ts +78 -78
  104. package/src/db/repositories/notification.repository.ts +66 -66
  105. package/src/db/repositories/project.repository.ts +93 -93
  106. package/src/db/repositories/rule.repository.ts +108 -108
  107. package/src/db/repositories/solution.repository.ts +154 -154
  108. package/src/db/repositories/synapse.repository.ts +153 -153
  109. package/src/db/repositories/terminal.repository.ts +101 -101
  110. package/src/hooks/post-tool-use.ts +90 -90
  111. package/src/hooks/post-write.ts +117 -117
  112. package/src/index.ts +4 -0
  113. package/src/ipc/client.ts +118 -118
  114. package/src/ipc/protocol.ts +35 -35
  115. package/src/ipc/router.ts +9 -1
  116. package/src/ipc/server.ts +110 -110
  117. package/src/learning/confidence-scorer.ts +47 -47
  118. package/src/learning/decay.ts +46 -46
  119. package/src/learning/learning-engine.ts +162 -162
  120. package/src/learning/pattern-extractor.ts +90 -90
  121. package/src/learning/rule-generator.ts +74 -74
  122. package/src/matching/error-matcher.ts +115 -115
  123. package/src/matching/fingerprint.ts +29 -29
  124. package/src/matching/similarity.ts +61 -61
  125. package/src/matching/tfidf.ts +74 -74
  126. package/src/matching/tokenizer.ts +41 -41
  127. package/src/mcp/auto-detect.ts +93 -93
  128. package/src/mcp/server.ts +73 -73
  129. package/src/mcp/tools.ts +290 -290
  130. package/src/parsing/error-parser.ts +28 -28
  131. package/src/parsing/parsers/compiler.ts +93 -93
  132. package/src/parsing/parsers/generic.ts +28 -28
  133. package/src/parsing/parsers/go.ts +97 -97
  134. package/src/parsing/parsers/node.ts +69 -69
  135. package/src/parsing/parsers/python.ts +62 -62
  136. package/src/parsing/parsers/rust.ts +50 -50
  137. package/src/parsing/parsers/shell.ts +42 -42
  138. package/src/parsing/types.ts +47 -47
  139. package/src/research/gap-analyzer.ts +135 -135
  140. package/src/research/insight-generator.ts +123 -123
  141. package/src/research/research-engine.ts +116 -116
  142. package/src/research/synergy-detector.ts +126 -126
  143. package/src/research/template-extractor.ts +130 -130
  144. package/src/research/trend-analyzer.ts +127 -127
  145. package/src/services/analytics.service.ts +87 -87
  146. package/src/services/code.service.ts +5 -2
  147. package/src/services/error.service.ts +164 -164
  148. package/src/services/notification.service.ts +41 -41
  149. package/src/services/prevention.service.ts +119 -119
  150. package/src/services/research.service.ts +93 -93
  151. package/src/services/solution.service.ts +116 -116
  152. package/src/services/synapse.service.ts +59 -59
  153. package/src/services/terminal.service.ts +81 -81
  154. package/src/synapses/activation.ts +80 -80
  155. package/src/synapses/decay.ts +38 -38
  156. package/src/synapses/hebbian.ts +69 -69
  157. package/src/synapses/pathfinder.ts +81 -81
  158. package/src/synapses/synapse-manager.ts +109 -109
  159. package/src/types/code.types.ts +52 -52
  160. package/src/types/config.types.ts +79 -79
  161. package/src/types/error.types.ts +67 -67
  162. package/src/types/ipc.types.ts +8 -8
  163. package/src/types/mcp.types.ts +53 -53
  164. package/src/types/research.types.ts +28 -28
  165. package/src/types/solution.types.ts +30 -30
  166. package/src/types/synapse.types.ts +49 -49
  167. package/src/utils/events.ts +45 -45
  168. package/src/utils/hash.ts +5 -5
  169. package/src/utils/logger.ts +48 -48
  170. package/src/utils/paths.ts +19 -19
  171. package/tests/fixtures/code-modules/modules.ts +83 -83
  172. package/tests/fixtures/errors/go.ts +9 -9
  173. package/tests/fixtures/errors/node.ts +24 -24
  174. package/tests/fixtures/errors/python.ts +21 -21
  175. package/tests/fixtures/errors/rust.ts +25 -25
  176. package/tests/fixtures/errors/shell.ts +15 -15
  177. package/tests/fixtures/solutions/solutions.ts +27 -27
  178. package/tests/helpers/setup-db.ts +52 -52
  179. package/tests/integration/code-flow.test.ts +86 -86
  180. package/tests/integration/error-flow.test.ts +83 -83
  181. package/tests/integration/ipc-flow.test.ts +166 -166
  182. package/tests/integration/learning-cycle.test.ts +82 -82
  183. package/tests/integration/synapse-flow.test.ts +117 -117
  184. package/tests/unit/code/analyzer.test.ts +58 -58
  185. package/tests/unit/code/fingerprint.test.ts +51 -51
  186. package/tests/unit/code/scorer.test.ts +55 -55
  187. package/tests/unit/learning/confidence-scorer.test.ts +60 -60
  188. package/tests/unit/learning/decay.test.ts +45 -45
  189. package/tests/unit/learning/pattern-extractor.test.ts +50 -50
  190. package/tests/unit/matching/error-matcher.test.ts +69 -69
  191. package/tests/unit/matching/fingerprint.test.ts +47 -47
  192. package/tests/unit/matching/similarity.test.ts +65 -65
  193. package/tests/unit/matching/tfidf.test.ts +71 -71
  194. package/tests/unit/matching/tokenizer.test.ts +83 -83
  195. package/tests/unit/parsing/parsers.test.ts +113 -113
  196. package/tests/unit/research/gap-analyzer.test.ts +45 -45
  197. package/tests/unit/research/trend-analyzer.test.ts +45 -45
  198. package/tests/unit/synapses/activation.test.ts +80 -80
  199. package/tests/unit/synapses/decay.test.ts +27 -27
  200. package/tests/unit/synapses/hebbian.test.ts +96 -96
  201. package/tests/unit/synapses/pathfinder.test.ts +72 -72
  202. package/tsconfig.json +18 -18
@@ -0,0 +1,169 @@
1
+ import { Command } from 'commander';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { getDataDir } from '../../utils/paths.js';
5
+ import { c, icons, header, divider } from '../colors.js';
6
+
7
+ function getConfigPath(): string {
8
+ return path.join(getDataDir(), 'config.json');
9
+ }
10
+
11
+ function readConfig(): Record<string, unknown> {
12
+ const configPath = getConfigPath();
13
+ if (fs.existsSync(configPath)) {
14
+ return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
15
+ }
16
+ return {};
17
+ }
18
+
19
+ function writeConfig(config: Record<string, unknown>): void {
20
+ const configPath = getConfigPath();
21
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
22
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
23
+ }
24
+
25
+ function getNestedValue(obj: Record<string, unknown>, keyPath: string): unknown {
26
+ const parts = keyPath.split('.');
27
+ let current: unknown = obj;
28
+ for (const part of parts) {
29
+ if (current === null || current === undefined || typeof current !== 'object') return undefined;
30
+ current = (current as Record<string, unknown>)[part];
31
+ }
32
+ return current;
33
+ }
34
+
35
+ function setNestedValue(obj: Record<string, unknown>, keyPath: string, value: unknown): void {
36
+ const parts = keyPath.split('.');
37
+ let current: Record<string, unknown> = obj;
38
+ for (let i = 0; i < parts.length - 1; i++) {
39
+ const part = parts[i];
40
+ if (!current[part] || typeof current[part] !== 'object') {
41
+ current[part] = {};
42
+ }
43
+ current = current[part] as Record<string, unknown>;
44
+ }
45
+ current[parts[parts.length - 1]] = value;
46
+ }
47
+
48
+ function deleteNestedValue(obj: Record<string, unknown>, keyPath: string): boolean {
49
+ const parts = keyPath.split('.');
50
+ let current: Record<string, unknown> = obj;
51
+ for (let i = 0; i < parts.length - 1; i++) {
52
+ const part = parts[i];
53
+ if (!current[part] || typeof current[part] !== 'object') return false;
54
+ current = current[part] as Record<string, unknown>;
55
+ }
56
+ const last = parts[parts.length - 1];
57
+ if (last in current) {
58
+ delete current[last];
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+
64
+ function parseValue(value: string): unknown {
65
+ if (value === 'true') return true;
66
+ if (value === 'false') return false;
67
+ if (value === 'null') return null;
68
+ const num = Number(value);
69
+ if (!isNaN(num) && value.trim() !== '') return num;
70
+ // Try JSON for arrays/objects
71
+ if ((value.startsWith('[') && value.endsWith(']')) || (value.startsWith('{') && value.endsWith('}'))) {
72
+ try { return JSON.parse(value); } catch { /* fall through */ }
73
+ }
74
+ return value;
75
+ }
76
+
77
+ function printObject(obj: unknown, indent = 0): void {
78
+ const pad = ' '.repeat(indent);
79
+ if (obj === null || obj === undefined) {
80
+ console.log(`${pad}${c.dim('(not set)')}`);
81
+ return;
82
+ }
83
+ if (typeof obj !== 'object') {
84
+ console.log(`${pad}${c.value(String(obj))}`);
85
+ return;
86
+ }
87
+ for (const [key, val] of Object.entries(obj as Record<string, unknown>)) {
88
+ if (val && typeof val === 'object' && !Array.isArray(val)) {
89
+ console.log(`${pad}${c.cyan(key + ':')}`);
90
+ printObject(val, indent + 2);
91
+ } else {
92
+ const display = Array.isArray(val) ? JSON.stringify(val) : String(val);
93
+ console.log(`${pad}${c.label(key + ':')} ${c.value(display)}`);
94
+ }
95
+ }
96
+ }
97
+
98
+ export function configCommand(): Command {
99
+ const cmd = new Command('config')
100
+ .description('View and modify Brain configuration');
101
+
102
+ cmd
103
+ .command('show')
104
+ .description('Show current configuration')
105
+ .argument('[key]', 'Specific config key (e.g., learning.intervalMs)')
106
+ .action((key?: string) => {
107
+ const config = readConfig();
108
+
109
+ if (key) {
110
+ const value = getNestedValue(config, key);
111
+ if (value === undefined) {
112
+ console.log(`${c.dim(`Key "${key}" is not set in config overrides.`)}`);
113
+ } else {
114
+ console.log(`${c.label(key + ':')} ${c.value(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value))}`);
115
+ }
116
+ return;
117
+ }
118
+
119
+ console.log(header('Brain Configuration', icons.gear));
120
+ console.log(` ${c.label('Config file:')} ${c.dim(getConfigPath())}\n`);
121
+
122
+ if (Object.keys(config).length === 0) {
123
+ console.log(` ${c.dim('No custom overrides. Using defaults.')}`);
124
+ console.log(` ${c.dim('Set values with:')} ${c.cyan('brain config set <key> <value>')}`);
125
+ } else {
126
+ printObject(config, 2);
127
+ }
128
+ console.log(`\n${divider()}`);
129
+ });
130
+
131
+ cmd
132
+ .command('set')
133
+ .description('Set a configuration value')
134
+ .argument('<key>', 'Config key path (e.g., learning.intervalMs)')
135
+ .argument('<value>', 'Value to set')
136
+ .action((key: string, value: string) => {
137
+ const config = readConfig();
138
+ const parsed = parseValue(value);
139
+ setNestedValue(config, key, parsed);
140
+ writeConfig(config);
141
+
142
+ console.log(`${icons.ok} ${c.label(key)} ${c.dim(icons.arrow)} ${c.value(String(parsed))}`);
143
+ console.log(` ${c.dim('Restart the daemon for changes to take effect:')} ${c.cyan('brain stop && brain start')}`);
144
+ });
145
+
146
+ cmd
147
+ .command('delete')
148
+ .description('Remove a configuration override (revert to default)')
149
+ .argument('<key>', 'Config key path to remove')
150
+ .action((key: string) => {
151
+ const config = readConfig();
152
+ if (deleteNestedValue(config, key)) {
153
+ writeConfig(config);
154
+ console.log(`${icons.ok} ${c.dim(`Removed "${key}" — will use default value.`)}`);
155
+ console.log(` ${c.dim('Restart the daemon for changes to take effect:')} ${c.cyan('brain stop && brain start')}`);
156
+ } else {
157
+ console.log(`${c.dim(`Key "${key}" not found in config overrides.`)}`);
158
+ }
159
+ });
160
+
161
+ cmd
162
+ .command('path')
163
+ .description('Show the config file path')
164
+ .action(() => {
165
+ console.log(getConfigPath());
166
+ });
167
+
168
+ return cmd;
169
+ }
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
3
  import { writeFileSync } from 'fs';
4
4
  import { resolve } from 'path';
5
+ import { c, icons } from '../colors.js';
5
6
 
6
7
  export function dashboardCommand(): Command {
7
8
  return new Command('dashboard')
@@ -10,13 +11,15 @@ export function dashboardCommand(): Command {
10
11
  .option('--no-open', 'Generate without opening in browser')
11
12
  .action(async (opts) => {
12
13
  await withIpc(async (client) => {
13
- console.log('Fetching data from Brain...');
14
+ console.log(`${icons.chart} ${c.info('Fetching data from Brain...')}`);
14
15
 
15
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
17
  const summary: any = await client.request('analytics.summary', {});
17
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
19
  const network: any = await client.request('synapse.stats', {});
19
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ const networkOverview: any = await client.request('analytics.network', { limit: 50 });
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
23
  const insights: any = await client.request('research.insights', {
21
24
  activeOnly: true,
22
25
  limit: 500,
@@ -44,6 +47,9 @@ export function dashboardCommand(): Command {
44
47
  const warnings = insightList.filter((i: InsightItem) => i.type === 'warning');
45
48
  const synergies = insightList.filter((i: InsightItem) => i.type === 'synergy' || i.type === 'optimization');
46
49
 
50
+ // Build synapse graph data
51
+ const synapseEdges = Array.isArray(networkOverview?.strongestSynapses) ? networkOverview.strongestSynapses : [];
52
+
47
53
  const data = {
48
54
  stats: {
49
55
  modules: summary.modules?.total ?? 0,
@@ -55,6 +61,7 @@ export function dashboardCommand(): Command {
55
61
  },
56
62
  langStats,
57
63
  insights: { templates, suggestions, trends, gaps, warnings, synergies },
64
+ synapseEdges,
58
65
  };
59
66
 
60
67
  const html = generateHtml(data);
@@ -63,10 +70,8 @@ export function dashboardCommand(): Command {
63
70
  : resolve(import.meta.dirname, '../../../dashboard.html');
64
71
 
65
72
  writeFileSync(outPath, html, 'utf-8');
66
- console.log(`Dashboard written to ${outPath}`);
67
- console.log(` Modules: ${data.stats.modules}`);
68
- console.log(` Synapses: ${data.stats.synapses}`);
69
- console.log(` Insights: ${data.stats.insights}`);
73
+ console.log(`${icons.ok} ${c.success('Dashboard written to')} ${c.dim(outPath)}`);
74
+ 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)}`);
70
75
 
71
76
  if (opts.open !== false) {
72
77
  const { exec } = await import('child_process');
@@ -83,6 +88,13 @@ interface InsightItem {
83
88
  priority?: string;
84
89
  }
85
90
 
91
+ interface SynapseEdge {
92
+ source: string;
93
+ target: string;
94
+ type: string;
95
+ weight: number;
96
+ }
97
+
86
98
  interface DashboardData {
87
99
  stats: {
88
100
  modules: number;
@@ -101,6 +113,7 @@ interface DashboardData {
101
113
  warnings: InsightItem[];
102
114
  synergies: InsightItem[];
103
115
  };
116
+ synapseEdges: SynapseEdge[];
104
117
  }
105
118
 
106
119
  function esc(s: string): string {
@@ -108,7 +121,7 @@ function esc(s: string): string {
108
121
  }
109
122
 
110
123
  function generateHtml(data: DashboardData): string {
111
- const { stats, langStats, insights } = data;
124
+ const { stats, langStats, insights, synapseEdges } = data;
112
125
 
113
126
  // Build language chart bars
114
127
  const sortedLangs = Object.entries(langStats).sort((a, b) => b[1] - a[1]);
@@ -276,6 +289,14 @@ function generateHtml(data: DashboardData): string {
276
289
  .prio-low{background:rgba(139,143,176,.1);color:var(--text2);border:1px solid rgba(139,143,176,.2)}
277
290
  .empty{color:var(--text3);font-style:italic;padding:24px}
278
291
 
292
+ /* Graph */
293
+ .graph-container{position:relative;background:var(--glass);border:1px solid var(--glass-border);border-radius:var(--radius);overflow:hidden;backdrop-filter:blur(20px)}
294
+ #synapse-graph{width:100%;height:500px;display:block;cursor:grab}
295
+ #synapse-graph:active{cursor:grabbing}
296
+ .graph-legend{display:flex;gap:16px;flex-wrap:wrap;padding:12px 20px;border-top:1px solid var(--glass-border);font-size:.8rem;color:var(--text2)}
297
+ .legend-dot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;vertical-align:middle}
298
+ .graph-tooltip{position:absolute;display:none;background:var(--bg2);border:1px solid var(--glass-border);border-radius:8px;padding:8px 14px;font-size:.8rem;color:var(--text);pointer-events:none;z-index:10;backdrop-filter:blur(20px);box-shadow:0 8px 30px rgba(0,0,0,.3)}
299
+
279
300
  /* Footer */
280
301
  footer{text-align:center;padding:40px 0;border-top:1px solid var(--glass-border)}
281
302
  footer p{color:var(--text3);font-size:.8rem}
@@ -310,7 +331,8 @@ function generateHtml(data: DashboardData): string {
310
331
  <nav class="reveal reveal-delay-1">
311
332
  <a href="#stats">Stats</a>
312
333
  <a href="#languages">Languages</a>
313
- <a href="#research" class="research">&#128300; Research</a>
334
+ <a href="#network">&#128300; Network</a>
335
+ <a href="#research" class="research">&#128161; Research</a>
314
336
  </nav>
315
337
 
316
338
  <section id="stats" class="reveal reveal-delay-2">
@@ -330,7 +352,22 @@ function generateHtml(data: DashboardData): string {
330
352
  <div class="lang-chart">${langBars}</div>
331
353
  </section>
332
354
 
333
- <section id="research" class="reveal reveal-delay-4">
355
+ <section id="network" class="reveal reveal-delay-4">
356
+ <div class="section-title"><div class="icon" style="background:rgba(71,229,255,.1)">&#128300;</div> Synapse Network</div>
357
+ <div class="graph-container">
358
+ <canvas id="synapse-graph"></canvas>
359
+ <div class="graph-legend">
360
+ <span><span class="legend-dot" style="background:var(--blue)"></span> error</span>
361
+ <span><span class="legend-dot" style="background:var(--green)"></span> solution</span>
362
+ <span><span class="legend-dot" style="background:var(--purple)"></span> code_module</span>
363
+ <span><span class="legend-dot" style="background:var(--orange)"></span> project</span>
364
+ <span><span class="legend-dot" style="background:var(--cyan)"></span> other</span>
365
+ </div>
366
+ <div id="graph-tooltip" class="graph-tooltip"></div>
367
+ </div>
368
+ </section>
369
+
370
+ <section id="research" class="reveal reveal-delay-5">
334
371
  <div class="section-title"><div class="icon" style="background:rgba(71,229,255,.1)">&#128300;</div> Research Insights</div>
335
372
  <div class="tab-bar">
336
373
  <button class="tab-btn active" data-tab="templates">&#127912; Templates <span class="count">${insights.templates.length}</span></button>
@@ -489,6 +526,192 @@ setTimeout(() => {
489
526
  el.style.width = el.dataset.target + '%';
490
527
  });
491
528
  }, 500);
529
+
530
+ // --- Synapse Force-Directed Graph ---
531
+ (function(){
532
+ const edges = ${JSON.stringify(synapseEdges.map((e: SynapseEdge) => ({ s: e.source, t: e.target, type: e.type, w: e.weight })))};
533
+ const canvas = document.getElementById('synapse-graph');
534
+ if (!canvas || !edges.length) return;
535
+ const ctx = canvas.getContext('2d');
536
+ const container = canvas.parentElement;
537
+ let W, H, dpr;
538
+
539
+ const NODE_COLORS = {
540
+ error: '#ff5577', solution: '#3dffa0', code_module: '#b47aff',
541
+ project: '#ffb347', rule: '#5b9cff', antipattern: '#ff5577'
542
+ };
543
+ const DEFAULT_COLOR = '#47e5ff';
544
+
545
+ // Build graph nodes & edges
546
+ const nodeMap = new Map();
547
+ const graphEdges = [];
548
+ for (const e of edges) {
549
+ if (!nodeMap.has(e.s)) nodeMap.set(e.s, { id: e.s, type: e.s.split(':')[0], x: 0, y: 0, vx: 0, vy: 0, connections: 0 });
550
+ if (!nodeMap.has(e.t)) nodeMap.set(e.t, { id: e.t, type: e.t.split(':')[0], x: 0, y: 0, vx: 0, vy: 0, connections: 0 });
551
+ nodeMap.get(e.s).connections++;
552
+ nodeMap.get(e.t).connections++;
553
+ graphEdges.push({ source: nodeMap.get(e.s), target: nodeMap.get(e.t), type: e.type, weight: e.w });
554
+ }
555
+ const nodes = [...nodeMap.values()];
556
+
557
+ function resize() {
558
+ dpr = window.devicePixelRatio || 1;
559
+ W = container.clientWidth;
560
+ H = 500;
561
+ canvas.width = W * dpr;
562
+ canvas.height = H * dpr;
563
+ canvas.style.width = W + 'px';
564
+ canvas.style.height = H + 'px';
565
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
566
+ }
567
+ resize();
568
+ window.addEventListener('resize', resize);
569
+
570
+ // Random initial positions
571
+ for (const n of nodes) {
572
+ n.x = W * 0.2 + Math.random() * W * 0.6;
573
+ n.y = H * 0.2 + Math.random() * H * 0.6;
574
+ }
575
+
576
+ // Force simulation
577
+ const REPULSION = 3000;
578
+ const ATTRACTION = 0.008;
579
+ const DAMPING = 0.85;
580
+ const CENTER_GRAVITY = 0.002;
581
+ let hovered = null;
582
+ let dragging = null;
583
+ let dragOff = {x:0,y:0};
584
+
585
+ function simulate() {
586
+ // Repulsion
587
+ for (let i = 0; i < nodes.length; i++) {
588
+ for (let j = i + 1; j < nodes.length; j++) {
589
+ let dx = nodes[i].x - nodes[j].x;
590
+ let dy = nodes[i].y - nodes[j].y;
591
+ let dist = Math.sqrt(dx*dx + dy*dy) || 1;
592
+ let force = REPULSION / (dist * dist);
593
+ let fx = (dx / dist) * force;
594
+ let fy = (dy / dist) * force;
595
+ nodes[i].vx += fx; nodes[i].vy += fy;
596
+ nodes[j].vx -= fx; nodes[j].vy -= fy;
597
+ }
598
+ }
599
+ // Attraction along edges
600
+ for (const e of graphEdges) {
601
+ let dx = e.target.x - e.source.x;
602
+ let dy = e.target.y - e.source.y;
603
+ let dist = Math.sqrt(dx*dx + dy*dy) || 1;
604
+ let force = (dist - 100) * ATTRACTION * e.weight;
605
+ let fx = (dx / dist) * force;
606
+ let fy = (dy / dist) * force;
607
+ e.source.vx += fx; e.source.vy += fy;
608
+ e.target.vx -= fx; e.target.vy -= fy;
609
+ }
610
+ // Center gravity
611
+ for (const n of nodes) {
612
+ n.vx += (W/2 - n.x) * CENTER_GRAVITY;
613
+ n.vy += (H/2 - n.y) * CENTER_GRAVITY;
614
+ }
615
+ // Apply & damp
616
+ for (const n of nodes) {
617
+ if (n === dragging) continue;
618
+ n.vx *= DAMPING; n.vy *= DAMPING;
619
+ n.x += n.vx; n.y += n.vy;
620
+ n.x = Math.max(20, Math.min(W - 20, n.x));
621
+ n.y = Math.max(20, Math.min(H - 20, n.y));
622
+ }
623
+ }
624
+
625
+ function getNodeRadius(n) { return Math.min(16, 5 + n.connections * 1.5); }
626
+
627
+ function draw() {
628
+ ctx.clearRect(0, 0, W, H);
629
+ // Edges
630
+ for (const e of graphEdges) {
631
+ const alpha = 0.15 + e.weight * 0.5;
632
+ ctx.strokeStyle = 'rgba(91,156,255,' + Math.min(0.8, alpha) + ')';
633
+ ctx.lineWidth = 0.5 + e.weight * 2;
634
+ ctx.beginPath();
635
+ ctx.moveTo(e.source.x, e.source.y);
636
+ ctx.lineTo(e.target.x, e.target.y);
637
+ ctx.stroke();
638
+ }
639
+ // Nodes
640
+ for (const n of nodes) {
641
+ const r = getNodeRadius(n);
642
+ const color = NODE_COLORS[n.type] || DEFAULT_COLOR;
643
+ const isHover = n === hovered || n === dragging;
644
+ // Glow
645
+ if (isHover) {
646
+ ctx.shadowColor = color;
647
+ ctx.shadowBlur = 20;
648
+ }
649
+ ctx.fillStyle = color;
650
+ ctx.globalAlpha = isHover ? 1 : 0.8;
651
+ ctx.beginPath();
652
+ ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
653
+ ctx.fill();
654
+ ctx.globalAlpha = 1;
655
+ ctx.shadowBlur = 0;
656
+ // Label for hovered or large nodes
657
+ if (isHover || n.connections >= 4) {
658
+ ctx.fillStyle = '#e8eaf6';
659
+ ctx.font = (isHover ? 'bold ' : '') + '11px Inter, system-ui, sans-serif';
660
+ ctx.textAlign = 'center';
661
+ ctx.fillText(n.id, n.x, n.y - r - 6);
662
+ }
663
+ }
664
+ simulate();
665
+ requestAnimationFrame(draw);
666
+ }
667
+ draw();
668
+
669
+ // Interaction
670
+ const tooltip = document.getElementById('graph-tooltip');
671
+ function getNodeAt(mx, my) {
672
+ for (let i = nodes.length - 1; i >= 0; i--) {
673
+ const n = nodes[i], r = getNodeRadius(n);
674
+ if (Math.hypot(mx - n.x, my - n.y) <= r + 4) return n;
675
+ }
676
+ return null;
677
+ }
678
+ function getPos(e) {
679
+ const rect = canvas.getBoundingClientRect();
680
+ return { x: e.clientX - rect.left, y: e.clientY - rect.top };
681
+ }
682
+ canvas.addEventListener('mousemove', function(e) {
683
+ const p = getPos(e);
684
+ if (dragging) {
685
+ dragging.x = p.x + dragOff.x;
686
+ dragging.y = p.y + dragOff.y;
687
+ dragging.vx = 0; dragging.vy = 0;
688
+ return;
689
+ }
690
+ const n = getNodeAt(p.x, p.y);
691
+ hovered = n;
692
+ canvas.style.cursor = n ? 'pointer' : 'grab';
693
+ if (n) {
694
+ const conns = graphEdges.filter(e => e.source === n || e.target === n);
695
+ tooltip.innerHTML = '<strong>' + n.id + '</strong><br>' + conns.length + ' connections';
696
+ tooltip.style.display = 'block';
697
+ tooltip.style.left = (p.x + 15) + 'px';
698
+ tooltip.style.top = (p.y - 10) + 'px';
699
+ } else {
700
+ tooltip.style.display = 'none';
701
+ }
702
+ });
703
+ canvas.addEventListener('mousedown', function(e) {
704
+ const p = getPos(e);
705
+ const n = getNodeAt(p.x, p.y);
706
+ if (n) {
707
+ dragging = n;
708
+ dragOff = { x: n.x - p.x, y: n.y - p.y };
709
+ canvas.style.cursor = 'grabbing';
710
+ }
711
+ });
712
+ canvas.addEventListener('mouseup', function() { dragging = null; });
713
+ canvas.addEventListener('mouseleave', function() { dragging = null; hovered = null; tooltip.style.display = 'none'; });
714
+ })();
492
715
  </script>
493
716
  </body>
494
717
  </html>`;
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons } from '../colors.js';
3
4
 
4
5
  export function exportCommand(): Command {
5
6
  return new Command('export')
@@ -7,6 +8,8 @@ export function exportCommand(): Command {
7
8
  .option('--format <fmt>', 'Output format: json (default)', 'json')
8
9
  .action(async () => {
9
10
  await withIpc(async (client) => {
11
+ process.stderr.write(`${icons.gear} ${c.info('Exporting Brain data...')}\n`);
12
+
10
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
14
  const summary: any = await client.request('analytics.summary', {});
12
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -22,6 +25,7 @@ export function exportCommand(): Command {
22
25
  };
23
26
 
24
27
  console.log(JSON.stringify(data, null, 2));
28
+ process.stderr.write(`${icons.ok} ${c.success('Export complete.')}\n`);
25
29
  });
26
30
  });
27
31
  }
@@ -2,10 +2,13 @@ import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
3
  import { readdirSync, readFileSync, statSync } from 'fs';
4
4
  import { resolve, basename, relative, extname } from 'path';
5
+ import { c, icons, header, divider, progressBar } from '../colors.js';
5
6
 
6
7
  const DEFAULT_EXTENSIONS = new Set([
7
8
  '.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go',
8
9
  '.java', '.c', '.cpp', '.h', '.hpp', '.rb', '.sh',
10
+ '.html', '.css', '.scss', '.json', '.yaml', '.yml', '.toml',
11
+ '.md', '.sql', '.php', '.svelte', '.vue', '.astro',
9
12
  ]);
10
13
 
11
14
  const EXCLUDE_DIRS = new Set([
@@ -22,6 +25,10 @@ const LANG_MAP: Record<string, string> = {
22
25
  py: 'python', rs: 'rust', go: 'go', java: 'java',
23
26
  c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp',
24
27
  rb: 'ruby', sh: 'shell', bash: 'shell',
28
+ html: 'html', css: 'css', scss: 'scss',
29
+ json: 'json', yaml: 'yaml', yml: 'yaml', toml: 'toml',
30
+ md: 'markdown', sql: 'sql', php: 'php',
31
+ svelte: 'svelte', vue: 'vue', astro: 'astro',
25
32
  };
26
33
 
27
34
  function detectLanguage(filePath: string): string {
@@ -75,7 +82,7 @@ export function importCommand(): Command {
75
82
  .description('Import source files from a project directory into Brain')
76
83
  .argument('<directory>', 'Project directory to scan')
77
84
  .option('-p, --project <name>', 'Project name (default: directory basename)')
78
- .option('-e, --extensions <list>', 'Comma-separated extensions (default: ts,tsx,js,jsx,py,rs,go,java,c,cpp,h,hpp,rb,sh)')
85
+ .option('-e, --extensions <list>', 'Comma-separated extensions (default: ts,tsx,js,jsx,py,rs,go,java,c,cpp,h,hpp,rb,sh,html,css,scss,json,yaml,yml,toml,md,sql,php,svelte,vue,astro)')
79
86
  .option('--dry-run', 'List files that would be imported without importing')
80
87
  .option('--max-size <kb>', 'Skip files larger than N KB', '100')
81
88
  .action(async (directory: string, opts) => {
@@ -107,23 +114,23 @@ export function importCommand(): Command {
107
114
  process.exit(1);
108
115
  }
109
116
 
110
- console.log(`Scanning ${dir} ...`);
117
+ console.log(`${icons.search} ${c.info('Scanning')} ${c.value(dir)} ...`);
111
118
  const files = findSourceFiles(dir, extensions, maxSizeBytes);
112
119
 
113
120
  if (files.length === 0) {
114
- console.log('No source files found.');
121
+ console.log(`${c.dim('No source files found.')}`);
115
122
  return;
116
123
  }
117
124
 
118
- console.log(`Found ${files.length} source files.\n`);
125
+ console.log(`${icons.ok} Found ${c.value(files.length)} source files.\n`);
119
126
 
120
127
  if (opts.dryRun) {
121
128
  for (const f of files) {
122
129
  const rel = relative(dir, f);
123
130
  const lang = detectLanguage(f);
124
- console.log(` [${lang}] ${rel}`);
131
+ console.log(` ${c.cyan(`[${lang}]`)} ${c.dim(rel)}`);
125
132
  }
126
- console.log(`\n${files.length} files would be imported as project "${projectName}".`);
133
+ console.log(`\n${c.value(files.length)} files would be imported as project ${c.cyan(`"${projectName}"`)}.`);
127
134
  return;
128
135
  }
129
136
 
@@ -146,7 +153,7 @@ export function importCommand(): Command {
146
153
  source = readFileSync(filePath, 'utf-8');
147
154
  } catch {
148
155
  failedCount++;
149
- process.stdout.write(` [${i + 1}/${files.length}] ${rel} — read error\n`);
156
+ process.stdout.write(` ${c.dim(`[${i + 1}/${files.length}]`)} ${c.dim(rel)} ${c.red('— read error')}\n`);
150
157
  continue;
151
158
  }
152
159
 
@@ -164,27 +171,29 @@ export function importCommand(): Command {
164
171
  });
165
172
 
166
173
  const score = result.reusabilityScore ?? 0;
167
- const status = result.isNew ? 'new' : 'existing';
174
+ const scoreColor = score >= 0.7 ? c.green : score >= 0.4 ? c.orange : c.red;
175
+ const statusTag = result.isNew ? c.green('new') : c.dim('existing');
168
176
  totalScore += score;
169
177
  imported++;
170
178
 
171
179
  if (result.isNew) newCount++;
172
180
  else existingCount++;
173
181
 
174
- process.stdout.write(` [${i + 1}/${files.length}] ${rel} score: ${score.toFixed(2)} (${status})\n`);
182
+ process.stdout.write(` ${c.dim(`[${i + 1}/${files.length}]`)} ${c.dim(rel)} ${c.dim(icons.arrow)} ${scoreColor(score.toFixed(2))} (${statusTag})\n`);
175
183
  } catch (err) {
176
184
  failedCount++;
177
185
  const msg = err instanceof Error ? err.message : String(err);
178
- process.stdout.write(` [${i + 1}/${files.length}] ${rel} failed: ${msg.slice(0, 80)}\n`);
186
+ process.stdout.write(` ${c.dim(`[${i + 1}/${files.length}]`)} ${c.dim(rel)} ${c.red(`— ${msg.slice(0, 80)}`)}\n`);
179
187
  }
180
188
  }
181
189
 
182
190
  const avgScore = imported > 0 ? (totalScore / imported).toFixed(2) : '0';
183
- console.log(`\n--- Import Summary ---`);
184
- console.log(` Project: ${projectName}`);
185
- console.log(` Imported: ${imported} (${newCount} new, ${existingCount} existing)`);
186
- if (failedCount > 0) console.log(` Failed: ${failedCount}`);
187
- console.log(` Avg reusability score: ${avgScore}`);
191
+ console.log(header('Import Summary', icons.module));
192
+ console.log(` ${c.label('Project:')} ${c.cyan(projectName)}`);
193
+ console.log(` ${c.label('Imported:')} ${c.value(imported)} (${c.green(`${newCount} new`)}, ${c.dim(`${existingCount} existing`)})`);
194
+ if (failedCount > 0) console.log(` ${c.label('Failed:')} ${c.red(failedCount)}`);
195
+ console.log(` ${c.label('Avg score:')} ${c.value(avgScore)} ${progressBar(parseFloat(avgScore), 1)}`);
196
+ console.log(divider());
188
197
  });
189
198
  });
190
199
  }