@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
@@ -1,5 +1,18 @@
1
1
  import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons, header, priorityBadge, divider } from '../colors.js';
4
+
5
+ const TYPE_ICONS: Record<string, string> = {
6
+ trend: '📈',
7
+ pattern: '🔄',
8
+ gap: '⚠',
9
+ synergy: '⚡',
10
+ optimization: '🎯',
11
+ template_candidate: '🎨',
12
+ project_suggestion: '💡',
13
+ warning: '🚨',
14
+ suggestion: '💡',
15
+ };
3
16
 
4
17
  export function insightsCommand(): Command {
5
18
  return new Command('insights')
@@ -16,18 +29,37 @@ export function insightsCommand(): Command {
16
29
  });
17
30
 
18
31
  if (!insights?.length) {
19
- console.log('No active insights.');
32
+ console.log(`${icons.insight} ${c.dim('No active insights.')}`);
20
33
  return;
21
34
  }
22
35
 
23
- console.log(`${insights.length} insights:\n`);
36
+ console.log(header(`${insights.length} Insights`, icons.insight));
37
+
38
+ // Group by type
39
+ const byType: Record<string, number> = {};
24
40
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
41
  for (const ins of insights as any[]) {
26
- const priority = ins.priority >= 8 ? 'HIGH' : ins.priority >= 5 ? 'MEDIUM' : 'LOW';
27
- console.log(` [${ins.type}] [${priority}] ${ins.title}`);
28
- if (ins.description) console.log(` ${ins.description.slice(0, 150)}`);
42
+ byType[ins.type] = (byType[ins.type] || 0) + 1;
43
+ }
44
+ const typeSummary = Object.entries(byType)
45
+ .sort((a, b) => b[1] - a[1])
46
+ .map(([t, count]) => `${TYPE_ICONS[t] ?? '•'} ${c.cyan(t)} ${c.dim(`(${count})`)}`)
47
+ .join(' ');
48
+ console.log(` ${typeSummary}\n`);
49
+
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ for (const ins of insights as any[]) {
52
+ const typeIcon = TYPE_ICONS[ins.type] ?? '•';
53
+ const pBadge = priorityBadge(ins.priority ?? 0);
54
+ const typeTag = c.cyan(`[${ins.type}]`);
55
+
56
+ console.log(` ${typeIcon} ${typeTag} ${pBadge} ${c.value(ins.title)}`);
57
+ if (ins.description) {
58
+ console.log(` ${c.dim(ins.description.slice(0, 150))}`);
59
+ }
29
60
  console.log();
30
61
  }
62
+ console.log(divider());
31
63
  });
32
64
  });
33
65
  }
@@ -0,0 +1,24 @@
1
+ import { Command } from 'commander';
2
+ import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons, header, keyValue, divider } from '../colors.js';
4
+
5
+ export function learnCommand(): Command {
6
+ return new Command('learn')
7
+ .description('Trigger a learning cycle manually (pattern extraction + rule generation)')
8
+ .action(async () => {
9
+ await withIpc(async (client) => {
10
+ console.log(`${icons.brain} ${c.info('Running learning cycle...')}`);
11
+
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ const result: any = await client.request('learning.run', {});
14
+
15
+ console.log(header('Learning Cycle Complete', icons.bolt));
16
+ console.log(keyValue('New patterns', result.newPatterns ?? 0));
17
+ console.log(keyValue('Updated rules', result.updatedRules ?? 0));
18
+ console.log(keyValue('Pruned rules', result.prunedRules ?? 0));
19
+ console.log(keyValue('New anti-patterns', result.newAntipatterns ?? 0));
20
+ console.log(keyValue('Duration', `${result.duration ?? 0}ms`));
21
+ console.log(`\n${divider()}`);
22
+ });
23
+ });
24
+ }
@@ -1,30 +1,53 @@
1
1
  import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons, header, divider } from '../colors.js';
3
4
 
4
5
  export function modulesCommand(): Command {
5
6
  return new Command('modules')
6
7
  .description('List registered code modules')
7
8
  .option('--language <lang>', 'Filter by language')
9
+ .option('-l, --limit <n>', 'Maximum results')
8
10
  .action(async (opts) => {
9
11
  await withIpc(async (client) => {
10
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
13
  const modules: any = await client.request('code.modules', {
12
14
  language: opts.language,
15
+ limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
13
16
  });
14
17
 
15
18
  if (!modules?.length) {
16
- console.log('No code modules registered.');
19
+ console.log(`${icons.module} ${c.dim('No code modules registered.')}`);
17
20
  return;
18
21
  }
19
22
 
20
- console.log(`${modules.length} code modules:\n`);
23
+ console.log(header(`${modules.length} Code Modules`, icons.module));
24
+
25
+ // Group by language
26
+ const byLang: Record<string, number> = {};
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ for (const mod of modules as any[]) {
29
+ byLang[mod.language] = (byLang[mod.language] || 0) + 1;
30
+ }
31
+ const langSummary = Object.entries(byLang)
32
+ .sort((a, b) => b[1] - a[1])
33
+ .map(([lang, count]) => `${c.cyan(lang)} ${c.dim(`(${count})`)}`)
34
+ .join(' ');
35
+ console.log(` ${langSummary}\n`);
36
+
21
37
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
38
  for (const mod of modules as any[]) {
23
- console.log(` #${mod.id} [${mod.language}] ${mod.name}`);
24
- if (mod.description) console.log(` ${mod.description.slice(0, 120)}`);
25
- console.log(` File: ${mod.filePath} Reusability: ${mod.reusabilityScore ?? '?'}`);
39
+ const score = mod.reusabilityScore ?? mod.reusability_score ?? 0;
40
+ const scoreColor = score >= 0.7 ? c.green : score >= 0.4 ? c.orange : c.red;
41
+ const langTag = c.cyan(`[${mod.language}]`);
42
+
43
+ console.log(` ${c.dim(`#${mod.id}`)} ${langTag} ${c.value(mod.name)}`);
44
+ if (mod.description) {
45
+ console.log(` ${c.dim(mod.description.slice(0, 120))}`);
46
+ }
47
+ console.log(` ${c.label('File:')} ${c.dim(mod.filePath ?? mod.file_path)} ${c.label('Score:')} ${scoreColor(typeof score === 'number' ? score.toFixed(2) : score)}`);
26
48
  console.log();
27
49
  }
50
+ console.log(divider());
28
51
  });
29
52
  });
30
53
  }
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons, header, keyValue, divider } from '../colors.js';
3
4
 
4
5
  export function networkCommand(): Command {
5
6
  return new Command('network')
@@ -13,7 +14,7 @@ export function networkCommand(): Command {
13
14
  const nodeId = parseInt(nodeIdStr, 10);
14
15
 
15
16
  if (!nodeType || isNaN(nodeId)) {
16
- console.error('Invalid node format. Use: --node error:42');
17
+ console.error(c.error('Invalid node format. Use: --node error:42'));
17
18
  return;
18
19
  }
19
20
 
@@ -25,14 +26,16 @@ export function networkCommand(): Command {
25
26
  });
26
27
 
27
28
  if (!related?.length) {
28
- console.log(`No connections found for ${nodeType}:${nodeId}`);
29
+ console.log(`${c.dim('No connections found for')} ${c.cyan(`${nodeType}:${nodeId}`)}`);
29
30
  return;
30
31
  }
31
32
 
32
- console.log(`Connections from ${nodeType}:${nodeId}:\n`);
33
+ console.log(header(`Connections from ${nodeType}:${nodeId}`, icons.synapse));
33
34
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
35
  for (const r of related as any[]) {
35
- console.log(` → ${r.nodeType}:${r.nodeId} (weight: ${(r.activation ?? r.weight ?? 0).toFixed(3)})`);
36
+ const weight = (r.activation ?? r.weight ?? 0);
37
+ const weightColor = weight >= 0.7 ? c.green : weight >= 0.3 ? c.orange : c.dim;
38
+ console.log(` ${c.cyan(icons.arrow)} ${c.value(`${r.nodeType}:${r.nodeId}`)} ${c.label('weight:')} ${weightColor(weight.toFixed(3))}`);
36
39
  }
37
40
  } else {
38
41
  // Show general network stats
@@ -43,19 +46,22 @@ export function networkCommand(): Command {
43
46
  limit: parseInt(opts.limit, 10),
44
47
  });
45
48
 
46
- console.log('Synapse Network Overview:\n');
47
- console.log(` Total synapses: ${stats.totalSynapses ?? 0}`);
48
- console.log(` Average weight: ${(stats.avgWeight ?? 0).toFixed(3)}`);
49
+ console.log(header('Synapse Network', icons.synapse));
50
+ console.log(keyValue('Total synapses', stats.totalSynapses ?? 0));
51
+ console.log(keyValue('Average weight', (stats.avgWeight ?? 0).toFixed(3)));
49
52
  console.log();
50
53
 
51
54
  if (overview?.strongestSynapses?.length) {
52
- console.log('Strongest connections:');
55
+ console.log(` ${c.purple.bold('Strongest connections:')}`);
53
56
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
57
  for (const s of overview.strongestSynapses as any[]) {
55
- console.log(` ${s.source} ${s.target} [${s.type}] weight: ${(s.weight ?? 0).toFixed(3)}`);
58
+ const weight = (s.weight ?? 0);
59
+ const weightColor = weight >= 0.7 ? c.green : weight >= 0.3 ? c.orange : c.dim;
60
+ console.log(` ${c.dim(s.source)} ${c.cyan(icons.arrow)} ${c.dim(s.target)} ${c.label(`[${s.type}]`)} ${weightColor(weight.toFixed(3))}`);
56
61
  }
57
62
  }
58
63
  }
64
+ console.log(`\n${divider()}`);
59
65
  });
60
66
  });
61
67
  }
@@ -1,42 +1,119 @@
1
1
  import { Command } from 'commander';
2
2
  import { withIpc } from '../ipc-helper.js';
3
+ import { c, icons, header, statusBadge, divider } from '../colors.js';
3
4
 
4
5
  export function queryCommand(): Command {
5
6
  return new Command('query')
6
- .description('Search for errors and solutions')
7
- .argument('<search>', 'Error message or description to search for')
8
- .option('-l, --limit <n>', 'Maximum results', '10')
7
+ .description('Search errors, code modules, and insights')
8
+ .argument('<search>', 'Search term')
9
+ .option('-l, --limit <n>', 'Maximum results per category', '10')
10
+ .option('--errors-only', 'Only search errors')
11
+ .option('--modules-only', 'Only search code modules')
12
+ .option('--insights-only', 'Only search insights')
13
+ .option('--page <n>', 'Page number (starting from 1)', '1')
9
14
  .action(async (search: string, opts) => {
10
15
  await withIpc(async (client) => {
11
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
- const results: any = await client.request('error.query', {
13
- search,
14
- limit: parseInt(opts.limit, 10),
15
- });
16
-
17
- if (!results?.length) {
18
- console.log('No matching errors found.');
19
- return;
16
+ const limit = parseInt(opts.limit, 10);
17
+ const page = parseInt(opts.page, 10) || 1;
18
+ const offset = (page - 1) * limit;
19
+ const searchAll = !opts.errorsOnly && !opts.modulesOnly && !opts.insightsOnly;
20
+ let totalResults = 0;
21
+
22
+ // --- Errors ---
23
+ if (searchAll || opts.errorsOnly) {
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const results: any = await client.request('error.query', {
26
+ search,
27
+ limit: limit + offset,
28
+ });
29
+
30
+ const errors = Array.isArray(results) ? results.slice(offset, offset + limit) : [];
31
+ if (errors.length > 0) {
32
+ totalResults += errors.length;
33
+ console.log(header(`Errors matching "${search}"`, icons.error));
34
+
35
+ for (const err of errors) {
36
+ const badge = statusBadge(err.resolved ? 'resolved' : 'open');
37
+ const typeTag = c.purple(err.errorType ?? 'unknown');
38
+ console.log(` ${c.dim(`#${err.id}`)} ${badge} ${typeTag}`);
39
+ console.log(` ${c.dim((err.message ?? '').slice(0, 120))}`);
40
+
41
+ // Get solutions for this error
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ const solutions: any = await client.request('solution.query', { error_id: err.id });
44
+ if (solutions?.length > 0) {
45
+ console.log(` ${c.green(`${icons.check} ${solutions.length} solution(s)`)}`);
46
+ for (const sol of solutions.slice(0, 3)) {
47
+ console.log(` ${c.dim(icons.corner)} ${c.dim((sol.description ?? '').slice(0, 100))}`);
48
+ }
49
+ }
50
+ console.log();
51
+ }
52
+ }
20
53
  }
21
54
 
22
- console.log(`Found ${results.length} errors:\n`);
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
- for (const err of results as any[]) {
25
- const status = err.resolved ? 'RESOLVED' : 'OPEN';
26
- console.log(` #${err.id} [${status}] ${err.errorType ?? 'unknown'}`);
27
- console.log(` ${(err.message ?? '').slice(0, 120)}`);
55
+ // --- Code Modules ---
56
+ if (searchAll || opts.modulesOnly) {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ const modules: any = await client.request('code.find', {
59
+ query: search,
60
+ limit: limit + offset,
61
+ });
62
+
63
+ const mods = Array.isArray(modules) ? modules.slice(offset, offset + limit) : [];
64
+ if (mods.length > 0) {
65
+ totalResults += mods.length;
66
+ console.log(header(`Modules matching "${search}"`, icons.module));
28
67
 
29
- // Get solutions for this error
68
+ for (const mod of mods) {
69
+ const score = mod.reusability_score ?? mod.reusabilityScore ?? 0;
70
+ const scoreColor = score >= 0.7 ? c.green : score >= 0.4 ? c.orange : c.red;
71
+ console.log(` ${c.dim(`#${mod.id}`)} ${c.cyan(`[${mod.language}]`)} ${c.value(mod.name)}`);
72
+ console.log(` ${c.label('File:')} ${c.dim(mod.file_path ?? mod.filePath)} ${c.label('Score:')} ${scoreColor(typeof score === 'number' ? score.toFixed(2) : score)}`);
73
+ if (mod.description) {
74
+ console.log(` ${c.dim(mod.description.slice(0, 120))}`);
75
+ }
76
+ console.log();
77
+ }
78
+ }
79
+ }
80
+
81
+ // --- Insights ---
82
+ if (searchAll || opts.insightsOnly) {
30
83
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
- const solutions: any = await client.request('solution.query', { error_id: err.id });
32
- if (solutions?.length > 0) {
33
- console.log(` Solutions: ${solutions.length}`);
34
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
- for (const sol of solutions.slice(0, 3) as any[]) {
36
- console.log(` - #${sol.id}: ${(sol.description ?? '').slice(0, 100)}`);
84
+ const insights: any = await client.request('research.insights', {
85
+ activeOnly: true,
86
+ limit: 100,
87
+ });
88
+
89
+ // Client-side search since insights API may not support text search
90
+ const allInsights = Array.isArray(insights) ? insights : [];
91
+ const searchLower = search.toLowerCase();
92
+ const matched = allInsights.filter((i: { title?: string; description?: string }) =>
93
+ (i.title ?? '').toLowerCase().includes(searchLower) ||
94
+ (i.description ?? '').toLowerCase().includes(searchLower)
95
+ ).slice(offset, offset + limit);
96
+
97
+ if (matched.length > 0) {
98
+ totalResults += matched.length;
99
+ console.log(header(`Insights matching "${search}"`, icons.insight));
100
+
101
+ for (const ins of matched) {
102
+ const typeTag = c.cyan(`[${ins.type}]`);
103
+ console.log(` ${typeTag} ${c.value(ins.title)}`);
104
+ if (ins.description) {
105
+ console.log(` ${c.dim(ins.description.slice(0, 150))}`);
106
+ }
107
+ console.log();
37
108
  }
38
109
  }
39
- console.log();
110
+ }
111
+
112
+ if (totalResults === 0) {
113
+ console.log(`\n${icons.search} ${c.dim(`No results found for "${search}".`)}`);
114
+ } else {
115
+ console.log(` ${c.dim(`Page ${page} — showing ${totalResults} result(s). Use --page ${page + 1} for more.`)}`);
116
+ console.log(divider());
40
117
  }
41
118
  });
42
119
  });
@@ -3,6 +3,8 @@ import { spawn } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { getDataDir } from '../../utils/paths.js';
6
+ import { c, icons } from '../colors.js';
7
+ import { checkForUpdate } from '../update-check.js';
6
8
 
7
9
  export function startCommand(): Command {
8
10
  return new Command('start')
@@ -17,7 +19,7 @@ export function startCommand(): Command {
17
19
  const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim(), 10);
18
20
  try {
19
21
  process.kill(pid, 0); // Check if process exists
20
- console.log(`Brain daemon is already running (PID: ${pid})`);
22
+ console.log(`${icons.brain} Brain daemon is ${c.green('already running')} ${c.dim(`(PID: ${pid})`)}`);
21
23
  return;
22
24
  } catch {
23
25
  // PID file stale, remove it
@@ -45,15 +47,16 @@ export function startCommand(): Command {
45
47
  });
46
48
  child.unref();
47
49
 
48
- console.log(`Brain daemon starting (PID: ${child.pid})`);
50
+ console.log(`${icons.brain} ${c.info('Brain daemon starting')} ${c.dim(`(PID: ${child.pid})`)}`);
49
51
 
50
52
  // Wait briefly for PID file to appear
51
- setTimeout(() => {
53
+ setTimeout(async () => {
52
54
  if (fs.existsSync(pidPath)) {
53
- console.log('Brain daemon started successfully.');
55
+ console.log(`${icons.ok} ${c.success('Brain daemon started successfully.')}`);
54
56
  } else {
55
- console.log('Brain daemon may still be starting. Check: brain status');
57
+ console.log(`${icons.clock} ${c.warn('Brain daemon may still be starting.')} Check: ${c.cyan('brain status')}`);
56
58
  }
59
+ await checkForUpdate();
57
60
  }, 1000);
58
61
  });
59
62
  }
@@ -3,6 +3,8 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getDataDir } from '../../utils/paths.js';
5
5
  import { withIpc } from '../ipc-helper.js';
6
+ import { c, icons, header, keyValue, divider } from '../colors.js';
7
+ import { checkForUpdate, getCurrentVersion } from '../update-check.js';
6
8
 
7
9
  export function statusCommand(): Command {
8
10
  return new Command('status')
@@ -11,7 +13,7 @@ export function statusCommand(): Command {
11
13
  const pidPath = path.join(getDataDir(), 'brain.pid');
12
14
 
13
15
  if (!fs.existsSync(pidPath)) {
14
- console.log('Brain Daemon: NOT RUNNING');
16
+ console.log(`${icons.brain} Brain Daemon: ${c.red.bold('NOT RUNNING')}`);
15
17
  return;
16
18
  }
17
19
 
@@ -23,11 +25,12 @@ export function statusCommand(): Command {
23
25
  } catch { /* not running */ }
24
26
 
25
27
  if (!running) {
26
- console.log('Brain Daemon: NOT RUNNING (stale PID file)');
28
+ console.log(`${icons.brain} Brain Daemon: ${c.red.bold('NOT RUNNING')} ${c.dim('(stale PID file)')}`);
27
29
  return;
28
30
  }
29
31
 
30
- console.log(`Brain Daemon: RUNNING (PID ${pid})`);
32
+ console.log(header(`Brain Status v${getCurrentVersion()}`, icons.brain));
33
+ console.log(` ${c.green(`${icons.dot} RUNNING`)} ${c.dim(`(PID ${pid})`)}`);
31
34
 
32
35
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
36
  await withIpc(async (client) => {
@@ -43,27 +46,30 @@ export function statusCommand(): Command {
43
46
  dbSize = `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
44
47
  } catch { /* ignore */ }
45
48
 
46
- console.log(`Database: ${dbPath} (${dbSize})`);
49
+ console.log(keyValue('Database', `${dbPath} (${dbSize})`));
47
50
  console.log();
48
51
 
49
- console.log('Error Brain:');
50
- console.log(` Errors: ${summary.errors?.total ?? 0} total, ${summary.errors?.unresolved ?? 0} unresolved, ${summary.errors?.last7d ?? 0} last 7d`);
51
- console.log(` Solutions: ${summary.solutions?.total ?? 0}`);
52
- console.log(` Rules: ${summary.rules?.active ?? 0} active`);
53
- console.log(` Anti-Patterns: ${summary.antipatterns?.total ?? 0}`);
52
+ console.log(` ${icons.error} ${c.purple.bold('Error Brain')}`);
53
+ console.log(` ${c.label('Errors:')} ${c.value(summary.errors?.total ?? 0)} total, ${c.red(summary.errors?.unresolved ?? 0)} unresolved, ${c.dim(`${summary.errors?.last7d ?? 0} last 7d`)}`);
54
+ console.log(` ${c.label('Solutions:')} ${c.value(summary.solutions?.total ?? 0)}`);
55
+ console.log(` ${c.label('Rules:')} ${c.green(summary.rules?.active ?? 0)} active`);
56
+ console.log(` ${c.label('Anti-Pat.:')} ${c.value(summary.antipatterns?.total ?? 0)}`);
54
57
  console.log();
55
58
 
56
- console.log('Code Brain:');
57
- console.log(` Modules: ${summary.modules?.total ?? 0} registered`);
59
+ console.log(` ${icons.module} ${c.blue.bold('Code Brain')}`);
60
+ console.log(` ${c.label('Modules:')} ${c.value(summary.modules?.total ?? 0)} registered`);
58
61
  console.log();
59
62
 
60
- console.log('Synapse Network:');
61
- console.log(` Synapses: ${network.totalSynapses ?? 0}`);
62
- console.log(` Avg weight: ${(network.avgWeight ?? 0).toFixed(2)}`);
63
+ console.log(` ${icons.synapse} ${c.cyan.bold('Synapse Network')}`);
64
+ console.log(` ${c.label('Synapses:')} ${c.value(network.totalSynapses ?? 0)}`);
65
+ console.log(` ${c.label('Avg weight:')} ${c.value((network.avgWeight ?? 0).toFixed(2))}`);
63
66
  console.log();
64
67
 
65
- console.log('Research Brain:');
66
- console.log(` Insights: ${summary.insights?.active ?? 0} active`);
68
+ console.log(` ${icons.insight} ${c.orange.bold('Research Brain')}`);
69
+ console.log(` ${c.label('Insights:')} ${c.value(summary.insights?.active ?? 0)} active`);
70
+
71
+ await checkForUpdate();
72
+ console.log(`\n${divider()}`);
67
73
  });
68
74
  });
69
75
  }
@@ -2,6 +2,7 @@ import { Command } from 'commander';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getDataDir } from '../../utils/paths.js';
5
+ import { c, icons } from '../colors.js';
5
6
 
6
7
  export function stopCommand(): Command {
7
8
  return new Command('stop')
@@ -10,7 +11,7 @@ export function stopCommand(): Command {
10
11
  const pidPath = path.join(getDataDir(), 'brain.pid');
11
12
 
12
13
  if (!fs.existsSync(pidPath)) {
13
- console.log('Brain daemon is not running (no PID file found).');
14
+ console.log(`${icons.brain} ${c.dim('Brain daemon is not running (no PID file found).')}`);
14
15
  return;
15
16
  }
16
17
 
@@ -18,12 +19,12 @@ export function stopCommand(): Command {
18
19
 
19
20
  try {
20
21
  process.kill(pid, 'SIGTERM');
21
- console.log(`Brain daemon stopped (PID: ${pid})`);
22
+ console.log(`${icons.brain} ${c.success('Brain daemon stopped')} ${c.dim(`(PID: ${pid})`)}`);
22
23
  } catch (err) {
23
24
  if ((err as NodeJS.ErrnoException).code === 'ESRCH') {
24
- console.log('Brain daemon was not running (stale PID file removed).');
25
+ console.log(`${icons.brain} ${c.dim('Brain daemon was not running (stale PID file removed).')}`);
25
26
  } else {
26
- console.error(`Failed to stop daemon: ${err}`);
27
+ console.error(`${icons.error} ${c.error(`Failed to stop daemon: ${err}`)}`);
27
28
  }
28
29
  }
29
30
 
@@ -1,5 +1,6 @@
1
1
  import { IpcClient } from '../ipc/client.js';
2
2
  import { getPipeName } from '../utils/paths.js';
3
+ import { c, icons } from './colors.js';
3
4
 
4
5
  export async function withIpc<T>(fn: (client: IpcClient) => Promise<T>): Promise<T> {
5
6
  const client = new IpcClient(getPipeName(), 5000);
@@ -8,11 +9,11 @@ export async function withIpc<T>(fn: (client: IpcClient) => Promise<T>): Promise
8
9
  return await fn(client);
9
10
  } catch (err) {
10
11
  if (err instanceof Error && err.message.includes('ENOENT')) {
11
- console.error('Brain daemon is not running. Start it with: brain start');
12
+ console.error(`${icons.error} ${c.error('Brain daemon is not running.')} Start it with: ${c.cyan('brain start')}`);
12
13
  } else if (err instanceof Error && err.message.includes('ECONNREFUSED')) {
13
- console.error('Brain daemon is not responding. Try: brain stop && brain start');
14
+ console.error(`${icons.error} ${c.error('Brain daemon is not responding.')} Try: ${c.cyan('brain stop && brain start')}`);
14
15
  } else {
15
- console.error(`Error: ${err instanceof Error ? err.message : err}`);
16
+ console.error(`${icons.error} ${c.error(err instanceof Error ? err.message : String(err))}`);
16
17
  }
17
18
  process.exit(1);
18
19
  } finally {
@@ -0,0 +1,63 @@
1
+ import https from 'node:https';
2
+ import { c, icons } from './colors.js';
3
+
4
+ // Read current version from package.json at build time
5
+ import { createRequire } from 'node:module';
6
+ const require = createRequire(import.meta.url);
7
+ const pkg = require('../../package.json');
8
+ const CURRENT_VERSION: string = pkg.version;
9
+
10
+ export function getCurrentVersion(): string {
11
+ return CURRENT_VERSION;
12
+ }
13
+
14
+ function fetchLatestVersion(): Promise<string | null> {
15
+ return new Promise((resolve) => {
16
+ const timeout = setTimeout(() => resolve(null), 3000);
17
+
18
+ const req = https.get(
19
+ 'https://registry.npmjs.org/@timmeck/brain/latest',
20
+ { headers: { Accept: 'application/json' } },
21
+ (res) => {
22
+ let data = '';
23
+ res.on('data', (chunk) => { data += chunk; });
24
+ res.on('end', () => {
25
+ clearTimeout(timeout);
26
+ try {
27
+ const json = JSON.parse(data);
28
+ resolve(json.version ?? null);
29
+ } catch {
30
+ resolve(null);
31
+ }
32
+ });
33
+ },
34
+ );
35
+ req.on('error', () => {
36
+ clearTimeout(timeout);
37
+ resolve(null);
38
+ });
39
+ });
40
+ }
41
+
42
+ function isNewer(latest: string, current: string): boolean {
43
+ const l = latest.split('.').map(Number);
44
+ const c = current.split('.').map(Number);
45
+ for (let i = 0; i < 3; i++) {
46
+ if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
47
+ if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
48
+ }
49
+ return false;
50
+ }
51
+
52
+ export async function checkForUpdate(): Promise<void> {
53
+ try {
54
+ const latest = await fetchLatestVersion();
55
+ if (latest && isNewer(latest, CURRENT_VERSION)) {
56
+ console.log();
57
+ console.log(` ${icons.star} ${c.orange.bold(`Update available: v${CURRENT_VERSION} → v${latest}`)}`);
58
+ console.log(` Run: ${c.cyan('npm update -g @timmeck/brain')}`);
59
+ }
60
+ } catch {
61
+ // silently ignore — update check is best-effort
62
+ }
63
+ }