@timmeck/brain 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/BRAIN_PLAN.md +3324 -3324
  2. package/LICENSE +21 -21
  3. package/dist/api/server.d.ts +4 -0
  4. package/dist/api/server.js +73 -0
  5. package/dist/api/server.js.map +1 -1
  6. package/dist/brain.js +2 -1
  7. package/dist/brain.js.map +1 -1
  8. package/dist/cli/commands/dashboard.js +606 -572
  9. package/dist/cli/commands/dashboard.js.map +1 -1
  10. package/dist/dashboard/server.js +25 -25
  11. package/dist/db/migrations/001_core_schema.js +115 -115
  12. package/dist/db/migrations/002_learning_schema.js +33 -33
  13. package/dist/db/migrations/003_code_schema.js +48 -48
  14. package/dist/db/migrations/004_synapses_schema.js +52 -52
  15. package/dist/db/migrations/005_fts_indexes.js +73 -73
  16. package/dist/db/migrations/007_feedback.js +8 -8
  17. package/dist/db/migrations/008_git_integration.js +33 -33
  18. package/dist/db/migrations/009_embeddings.js +3 -3
  19. package/dist/db/repositories/antipattern.repository.js +3 -3
  20. package/dist/db/repositories/code-module.repository.js +32 -32
  21. package/dist/db/repositories/notification.repository.js +3 -3
  22. package/dist/db/repositories/project.repository.js +21 -21
  23. package/dist/db/repositories/rule.repository.js +24 -24
  24. package/dist/db/repositories/solution.repository.js +50 -50
  25. package/dist/db/repositories/synapse.repository.js +18 -18
  26. package/dist/db/repositories/terminal.repository.js +24 -24
  27. package/dist/embeddings/engine.d.ts +2 -2
  28. package/dist/embeddings/engine.js +17 -4
  29. package/dist/embeddings/engine.js.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/ipc/server.d.ts +8 -0
  32. package/dist/ipc/server.js +67 -1
  33. package/dist/ipc/server.js.map +1 -1
  34. package/dist/matching/error-matcher.js +5 -5
  35. package/dist/matching/fingerprint.js +6 -1
  36. package/dist/matching/fingerprint.js.map +1 -1
  37. package/dist/mcp/http-server.js +8 -2
  38. package/dist/mcp/http-server.js.map +1 -1
  39. package/dist/services/code.service.d.ts +3 -0
  40. package/dist/services/code.service.js +33 -4
  41. package/dist/services/code.service.js.map +1 -1
  42. package/dist/services/error.service.js +4 -3
  43. package/dist/services/error.service.js.map +1 -1
  44. package/dist/services/git.service.js +14 -14
  45. package/package.json +49 -49
  46. package/src/api/server.ts +395 -321
  47. package/src/brain.ts +266 -265
  48. package/src/cli/colors.ts +116 -116
  49. package/src/cli/commands/config.ts +169 -169
  50. package/src/cli/commands/dashboard.ts +755 -720
  51. package/src/cli/commands/doctor.ts +118 -118
  52. package/src/cli/commands/explain.ts +83 -83
  53. package/src/cli/commands/export.ts +31 -31
  54. package/src/cli/commands/import.ts +199 -199
  55. package/src/cli/commands/insights.ts +65 -65
  56. package/src/cli/commands/learn.ts +24 -24
  57. package/src/cli/commands/modules.ts +53 -53
  58. package/src/cli/commands/network.ts +67 -67
  59. package/src/cli/commands/projects.ts +42 -42
  60. package/src/cli/commands/query.ts +120 -120
  61. package/src/cli/commands/start.ts +62 -62
  62. package/src/cli/commands/status.ts +75 -75
  63. package/src/cli/commands/stop.ts +34 -34
  64. package/src/cli/ipc-helper.ts +22 -22
  65. package/src/cli/update-check.ts +63 -63
  66. package/src/code/fingerprint.ts +87 -87
  67. package/src/code/parsers/generic.ts +29 -29
  68. package/src/code/parsers/python.ts +54 -54
  69. package/src/code/parsers/typescript.ts +65 -65
  70. package/src/code/registry.ts +60 -60
  71. package/src/dashboard/server.ts +142 -142
  72. package/src/db/connection.ts +22 -22
  73. package/src/db/migrations/001_core_schema.ts +120 -120
  74. package/src/db/migrations/002_learning_schema.ts +38 -38
  75. package/src/db/migrations/003_code_schema.ts +53 -53
  76. package/src/db/migrations/004_synapses_schema.ts +57 -57
  77. package/src/db/migrations/005_fts_indexes.ts +78 -78
  78. package/src/db/migrations/006_synapses_phase3.ts +17 -17
  79. package/src/db/migrations/007_feedback.ts +13 -13
  80. package/src/db/migrations/008_git_integration.ts +38 -38
  81. package/src/db/migrations/009_embeddings.ts +8 -8
  82. package/src/db/repositories/antipattern.repository.ts +66 -66
  83. package/src/db/repositories/code-module.repository.ts +142 -142
  84. package/src/db/repositories/notification.repository.ts +66 -66
  85. package/src/db/repositories/project.repository.ts +93 -93
  86. package/src/db/repositories/rule.repository.ts +108 -108
  87. package/src/db/repositories/solution.repository.ts +154 -154
  88. package/src/db/repositories/synapse.repository.ts +153 -153
  89. package/src/db/repositories/terminal.repository.ts +101 -101
  90. package/src/embeddings/engine.ts +238 -217
  91. package/src/index.ts +63 -63
  92. package/src/ipc/client.ts +118 -118
  93. package/src/ipc/protocol.ts +35 -35
  94. package/src/ipc/router.ts +133 -133
  95. package/src/ipc/server.ts +176 -110
  96. package/src/learning/decay.ts +46 -46
  97. package/src/learning/pattern-extractor.ts +90 -90
  98. package/src/learning/rule-generator.ts +74 -74
  99. package/src/matching/error-matcher.ts +5 -5
  100. package/src/matching/fingerprint.ts +34 -29
  101. package/src/matching/similarity.ts +61 -61
  102. package/src/matching/tfidf.ts +74 -74
  103. package/src/matching/tokenizer.ts +41 -41
  104. package/src/mcp/auto-detect.ts +93 -93
  105. package/src/mcp/http-server.ts +140 -137
  106. package/src/mcp/server.ts +73 -73
  107. package/src/parsing/error-parser.ts +28 -28
  108. package/src/parsing/parsers/compiler.ts +93 -93
  109. package/src/parsing/parsers/generic.ts +28 -28
  110. package/src/parsing/parsers/go.ts +97 -97
  111. package/src/parsing/parsers/node.ts +69 -69
  112. package/src/parsing/parsers/python.ts +62 -62
  113. package/src/parsing/parsers/rust.ts +50 -50
  114. package/src/parsing/parsers/shell.ts +42 -42
  115. package/src/parsing/types.ts +47 -47
  116. package/src/research/gap-analyzer.ts +135 -135
  117. package/src/research/insight-generator.ts +123 -123
  118. package/src/research/research-engine.ts +116 -116
  119. package/src/research/synergy-detector.ts +126 -126
  120. package/src/research/template-extractor.ts +130 -130
  121. package/src/research/trend-analyzer.ts +127 -127
  122. package/src/services/code.service.ts +271 -238
  123. package/src/services/error.service.ts +4 -3
  124. package/src/services/git.service.ts +132 -132
  125. package/src/services/notification.service.ts +41 -41
  126. package/src/services/synapse.service.ts +59 -59
  127. package/src/services/terminal.service.ts +81 -81
  128. package/src/synapses/activation.ts +80 -80
  129. package/src/synapses/decay.ts +38 -38
  130. package/src/synapses/hebbian.ts +69 -69
  131. package/src/synapses/pathfinder.ts +81 -81
  132. package/src/synapses/synapse-manager.ts +109 -109
  133. package/src/types/code.types.ts +52 -52
  134. package/src/types/error.types.ts +67 -67
  135. package/src/types/ipc.types.ts +8 -8
  136. package/src/types/mcp.types.ts +53 -53
  137. package/src/types/research.types.ts +28 -28
  138. package/src/types/solution.types.ts +30 -30
  139. package/src/utils/events.ts +45 -45
  140. package/src/utils/hash.ts +5 -5
  141. package/src/utils/logger.ts +48 -48
  142. package/src/utils/paths.ts +19 -19
  143. package/tests/e2e/test_code_intelligence.py +1015 -0
  144. package/tests/e2e/test_error_memory.py +451 -0
  145. package/tests/e2e/test_full_integration.py +534 -0
  146. package/tests/fixtures/code-modules/modules.ts +83 -83
  147. package/tests/fixtures/errors/go.ts +9 -9
  148. package/tests/fixtures/errors/node.ts +24 -24
  149. package/tests/fixtures/errors/python.ts +21 -21
  150. package/tests/fixtures/errors/rust.ts +25 -25
  151. package/tests/fixtures/errors/shell.ts +15 -15
  152. package/tests/fixtures/solutions/solutions.ts +27 -27
  153. package/tests/helpers/setup-db.ts +52 -52
  154. package/tests/integration/code-flow.test.ts +86 -86
  155. package/tests/integration/error-flow.test.ts +83 -83
  156. package/tests/integration/ipc-flow.test.ts +166 -166
  157. package/tests/integration/learning-cycle.test.ts +82 -82
  158. package/tests/integration/synapse-flow.test.ts +117 -117
  159. package/tests/unit/code/analyzer.test.ts +58 -58
  160. package/tests/unit/code/fingerprint.test.ts +51 -51
  161. package/tests/unit/code/scorer.test.ts +55 -55
  162. package/tests/unit/learning/confidence-scorer.test.ts +60 -60
  163. package/tests/unit/learning/decay.test.ts +45 -45
  164. package/tests/unit/learning/pattern-extractor.test.ts +50 -50
  165. package/tests/unit/matching/error-matcher.test.ts +69 -69
  166. package/tests/unit/matching/fingerprint.test.ts +47 -47
  167. package/tests/unit/matching/similarity.test.ts +65 -65
  168. package/tests/unit/matching/tfidf.test.ts +71 -71
  169. package/tests/unit/matching/tokenizer.test.ts +83 -83
  170. package/tests/unit/parsing/parsers.test.ts +113 -113
  171. package/tests/unit/research/gap-analyzer.test.ts +45 -45
  172. package/tests/unit/research/trend-analyzer.test.ts +45 -45
  173. package/tests/unit/synapses/activation.test.ts +80 -80
  174. package/tests/unit/synapses/decay.test.ts +27 -27
  175. package/tests/unit/synapses/hebbian.test.ts +96 -96
  176. package/tests/unit/synapses/pathfinder.test.ts +72 -72
  177. package/tsconfig.json +18 -18
package/src/cli/colors.ts CHANGED
@@ -1,116 +1,116 @@
1
- import chalk from 'chalk';
2
-
3
- // Brand colors matching the dashboard
4
- export const c = {
5
- // Primary palette
6
- blue: chalk.hex('#5b9cff'),
7
- purple: chalk.hex('#b47aff'),
8
- cyan: chalk.hex('#47e5ff'),
9
- green: chalk.hex('#3dffa0'),
10
- red: chalk.hex('#ff5577'),
11
- orange: chalk.hex('#ffb347'),
12
- dim: chalk.hex('#8b8fb0'),
13
- dimmer: chalk.hex('#4a4d6e'),
14
-
15
- // Semantic
16
- label: chalk.hex('#8b8fb0'),
17
- value: chalk.white.bold,
18
- heading: chalk.hex('#5b9cff').bold,
19
- success: chalk.hex('#3dffa0').bold,
20
- error: chalk.hex('#ff5577').bold,
21
- warn: chalk.hex('#ffb347').bold,
22
- info: chalk.hex('#47e5ff'),
23
- };
24
-
25
- export const icons = {
26
- brain: '🧠',
27
- check: '✓',
28
- cross: '✗',
29
- arrow: '→',
30
- dot: '●',
31
- circle: '○',
32
- bar: '█',
33
- barLight: '░',
34
- dash: '─',
35
- pipe: '│',
36
- corner: '└',
37
- tee: '├',
38
- star: '★',
39
- bolt: '⚡',
40
- search: '🔍',
41
- gear: '⚙',
42
- chart: '📊',
43
- module: '📦',
44
- synapse: '🔗',
45
- insight: '💡',
46
- warn: '⚠',
47
- error: '❌',
48
- ok: '✅',
49
- clock: '⏱',
50
- };
51
-
52
- export function header(title: string, icon?: string): string {
53
- const prefix = icon ? `${icon} ` : '';
54
- const line = c.dimmer(icons.dash.repeat(40));
55
- return `\n${line}\n${prefix}${c.heading(title)}\n${line}`;
56
- }
57
-
58
- export function keyValue(key: string, value: string | number, indent = 2): string {
59
- const pad = ' '.repeat(indent);
60
- return `${pad}${c.label(key + ':')} ${c.value(String(value))}`;
61
- }
62
-
63
- export function statusBadge(status: string): string {
64
- switch (status.toLowerCase()) {
65
- case 'resolved':
66
- case 'active':
67
- case 'running':
68
- return c.green(`[${status.toUpperCase()}]`);
69
- case 'open':
70
- case 'unresolved':
71
- return c.red(`[${status.toUpperCase()}]`);
72
- case 'warning':
73
- return c.warn(`[${status.toUpperCase()}]`);
74
- default:
75
- return c.dim(`[${status.toUpperCase()}]`);
76
- }
77
- }
78
-
79
- export function priorityBadge(priority: number | string): string {
80
- const p = typeof priority === 'string' ? priority.toLowerCase() : '';
81
- const n = typeof priority === 'number' ? priority : 0;
82
- if (p === 'critical' || n >= 9) return c.red.bold(`[CRITICAL]`);
83
- if (p === 'high' || n >= 7) return c.orange.bold(`[HIGH]`);
84
- if (p === 'medium' || n >= 4) return c.blue(`[MEDIUM]`);
85
- return c.dim(`[LOW]`);
86
- }
87
-
88
- export function progressBar(current: number, total: number, width = 20): string {
89
- const pct = Math.min(1, current / Math.max(1, total));
90
- const filled = Math.round(pct * width);
91
- const empty = width - filled;
92
- return c.cyan(icons.bar.repeat(filled)) + c.dimmer(icons.barLight.repeat(empty));
93
- }
94
-
95
- export function divider(width = 40): string {
96
- return c.dimmer(icons.dash.repeat(width));
97
- }
98
-
99
- export function table(rows: string[][], colWidths?: number[]): string {
100
- if (rows.length === 0) return '';
101
- const widths = colWidths ?? rows[0].map((_, i) =>
102
- Math.max(...rows.map(r => stripAnsi(r[i] ?? '').length))
103
- );
104
- return rows.map(row =>
105
- row.map((cell, i) => {
106
- const stripped = stripAnsi(cell);
107
- const pad = Math.max(0, (widths[i] ?? stripped.length) - stripped.length);
108
- return cell + ' '.repeat(pad);
109
- }).join(' ')
110
- ).join('\n');
111
- }
112
-
113
- function stripAnsi(str: string): string {
114
- // eslint-disable-next-line no-control-regex
115
- return str.replace(/\x1b\[[0-9;]*m/g, '');
116
- }
1
+ import chalk from 'chalk';
2
+
3
+ // Brand colors matching the dashboard
4
+ export const c = {
5
+ // Primary palette
6
+ blue: chalk.hex('#5b9cff'),
7
+ purple: chalk.hex('#b47aff'),
8
+ cyan: chalk.hex('#47e5ff'),
9
+ green: chalk.hex('#3dffa0'),
10
+ red: chalk.hex('#ff5577'),
11
+ orange: chalk.hex('#ffb347'),
12
+ dim: chalk.hex('#8b8fb0'),
13
+ dimmer: chalk.hex('#4a4d6e'),
14
+
15
+ // Semantic
16
+ label: chalk.hex('#8b8fb0'),
17
+ value: chalk.white.bold,
18
+ heading: chalk.hex('#5b9cff').bold,
19
+ success: chalk.hex('#3dffa0').bold,
20
+ error: chalk.hex('#ff5577').bold,
21
+ warn: chalk.hex('#ffb347').bold,
22
+ info: chalk.hex('#47e5ff'),
23
+ };
24
+
25
+ export const icons = {
26
+ brain: '🧠',
27
+ check: '✓',
28
+ cross: '✗',
29
+ arrow: '→',
30
+ dot: '●',
31
+ circle: '○',
32
+ bar: '█',
33
+ barLight: '░',
34
+ dash: '─',
35
+ pipe: '│',
36
+ corner: '└',
37
+ tee: '├',
38
+ star: '★',
39
+ bolt: '⚡',
40
+ search: '🔍',
41
+ gear: '⚙',
42
+ chart: '📊',
43
+ module: '📦',
44
+ synapse: '🔗',
45
+ insight: '💡',
46
+ warn: '⚠',
47
+ error: '❌',
48
+ ok: '✅',
49
+ clock: '⏱',
50
+ };
51
+
52
+ export function header(title: string, icon?: string): string {
53
+ const prefix = icon ? `${icon} ` : '';
54
+ const line = c.dimmer(icons.dash.repeat(40));
55
+ return `\n${line}\n${prefix}${c.heading(title)}\n${line}`;
56
+ }
57
+
58
+ export function keyValue(key: string, value: string | number, indent = 2): string {
59
+ const pad = ' '.repeat(indent);
60
+ return `${pad}${c.label(key + ':')} ${c.value(String(value))}`;
61
+ }
62
+
63
+ export function statusBadge(status: string): string {
64
+ switch (status.toLowerCase()) {
65
+ case 'resolved':
66
+ case 'active':
67
+ case 'running':
68
+ return c.green(`[${status.toUpperCase()}]`);
69
+ case 'open':
70
+ case 'unresolved':
71
+ return c.red(`[${status.toUpperCase()}]`);
72
+ case 'warning':
73
+ return c.warn(`[${status.toUpperCase()}]`);
74
+ default:
75
+ return c.dim(`[${status.toUpperCase()}]`);
76
+ }
77
+ }
78
+
79
+ export function priorityBadge(priority: number | string): string {
80
+ const p = typeof priority === 'string' ? priority.toLowerCase() : '';
81
+ const n = typeof priority === 'number' ? priority : 0;
82
+ if (p === 'critical' || n >= 9) return c.red.bold(`[CRITICAL]`);
83
+ if (p === 'high' || n >= 7) return c.orange.bold(`[HIGH]`);
84
+ if (p === 'medium' || n >= 4) return c.blue(`[MEDIUM]`);
85
+ return c.dim(`[LOW]`);
86
+ }
87
+
88
+ export function progressBar(current: number, total: number, width = 20): string {
89
+ const pct = Math.min(1, current / Math.max(1, total));
90
+ const filled = Math.round(pct * width);
91
+ const empty = width - filled;
92
+ return c.cyan(icons.bar.repeat(filled)) + c.dimmer(icons.barLight.repeat(empty));
93
+ }
94
+
95
+ export function divider(width = 40): string {
96
+ return c.dimmer(icons.dash.repeat(width));
97
+ }
98
+
99
+ export function table(rows: string[][], colWidths?: number[]): string {
100
+ if (rows.length === 0) return '';
101
+ const widths = colWidths ?? rows[0].map((_, i) =>
102
+ Math.max(...rows.map(r => stripAnsi(r[i] ?? '').length))
103
+ );
104
+ return rows.map(row =>
105
+ row.map((cell, i) => {
106
+ const stripped = stripAnsi(cell);
107
+ const pad = Math.max(0, (widths[i] ?? stripped.length) - stripped.length);
108
+ return cell + ' '.repeat(pad);
109
+ }).join(' ')
110
+ ).join('\n');
111
+ }
112
+
113
+ function stripAnsi(str: string): string {
114
+ // eslint-disable-next-line no-control-regex
115
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
116
+ }
@@ -1,169 +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
- }
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
+ }