brain-dev 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/agents/brain-checker.md +33 -0
- package/agents/brain-debugger.md +35 -0
- package/agents/brain-executor.md +37 -0
- package/agents/brain-mapper.md +44 -0
- package/agents/brain-planner.md +49 -0
- package/agents/brain-researcher.md +47 -0
- package/agents/brain-synthesizer.md +43 -0
- package/agents/brain-verifier.md +41 -0
- package/bin/brain-tools.cjs +185 -0
- package/bin/lib/adr.cjs +283 -0
- package/bin/lib/agents.cjs +152 -0
- package/bin/lib/anti-patterns.cjs +183 -0
- package/bin/lib/audit.cjs +268 -0
- package/bin/lib/commands/adr.cjs +126 -0
- package/bin/lib/commands/complete.cjs +270 -0
- package/bin/lib/commands/config.cjs +306 -0
- package/bin/lib/commands/discuss.cjs +237 -0
- package/bin/lib/commands/execute.cjs +415 -0
- package/bin/lib/commands/health.cjs +103 -0
- package/bin/lib/commands/map.cjs +101 -0
- package/bin/lib/commands/new-project.cjs +885 -0
- package/bin/lib/commands/pause.cjs +142 -0
- package/bin/lib/commands/phase-manage.cjs +357 -0
- package/bin/lib/commands/plan.cjs +451 -0
- package/bin/lib/commands/progress.cjs +167 -0
- package/bin/lib/commands/quick.cjs +447 -0
- package/bin/lib/commands/resume.cjs +196 -0
- package/bin/lib/commands/storm.cjs +590 -0
- package/bin/lib/commands/verify.cjs +504 -0
- package/bin/lib/commands.cjs +263 -0
- package/bin/lib/complexity.cjs +138 -0
- package/bin/lib/complexity.test.cjs +108 -0
- package/bin/lib/config.cjs +452 -0
- package/bin/lib/core.cjs +62 -0
- package/bin/lib/detect.cjs +603 -0
- package/bin/lib/git.cjs +112 -0
- package/bin/lib/health.cjs +356 -0
- package/bin/lib/init.cjs +310 -0
- package/bin/lib/logger.cjs +100 -0
- package/bin/lib/platform.cjs +58 -0
- package/bin/lib/requirements.cjs +158 -0
- package/bin/lib/roadmap.cjs +228 -0
- package/bin/lib/security.cjs +237 -0
- package/bin/lib/state.cjs +353 -0
- package/bin/lib/templates.cjs +48 -0
- package/bin/templates/advocate.md +182 -0
- package/bin/templates/checkpoint.md +55 -0
- package/bin/templates/debugger.md +148 -0
- package/bin/templates/discuss.md +60 -0
- package/bin/templates/executor.md +201 -0
- package/bin/templates/mapper.md +129 -0
- package/bin/templates/plan-checker.md +134 -0
- package/bin/templates/planner.md +165 -0
- package/bin/templates/researcher.md +78 -0
- package/bin/templates/storm.html +376 -0
- package/bin/templates/synthesis.md +30 -0
- package/bin/templates/verifier.md +181 -0
- package/commands/brain/adr.md +34 -0
- package/commands/brain/complete.md +37 -0
- package/commands/brain/config.md +37 -0
- package/commands/brain/discuss.md +35 -0
- package/commands/brain/execute.md +38 -0
- package/commands/brain/health.md +33 -0
- package/commands/brain/map.md +35 -0
- package/commands/brain/new-project.md +38 -0
- package/commands/brain/pause.md +26 -0
- package/commands/brain/plan.md +38 -0
- package/commands/brain/progress.md +28 -0
- package/commands/brain/quick.md +51 -0
- package/commands/brain/resume.md +28 -0
- package/commands/brain/storm.md +30 -0
- package/commands/brain/verify.md +39 -0
- package/hooks/bootstrap.sh +54 -0
- package/hooks/post-tool-use.sh +45 -0
- package/hooks/statusline.sh +130 -0
- package/package.json +36 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const { readState, writeState, atomicWriteSync } = require('./state.cjs');
|
|
7
|
+
|
|
8
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const CATEGORIES = ['Workflow', 'Models', 'Enforcement', 'Monitoring', 'Complexity', 'Storm', 'ADR'];
|
|
11
|
+
|
|
12
|
+
const SCHEMA = {
|
|
13
|
+
'mode': { type: 'enum', values: ['interactive', 'yolo'], default: 'interactive', category: 'Workflow', description: 'Execution mode preset (sets multiple flags)' },
|
|
14
|
+
'depth': { type: 'enum', values: ['shallow', 'standard', 'deep'], default: 'deep', category: 'Workflow', description: 'Verification depth (shallow=L1, standard=L1+L2, deep=L1+L2+L3)' },
|
|
15
|
+
'workflow.parallelization': { type: 'boolean', default: false, category: 'Workflow', description: 'Enable parallel wave execution' },
|
|
16
|
+
'workflow.mapper_parallelization': { type: 'boolean', default: true, category: 'Workflow', description: 'Enable parallel codebase mapping' },
|
|
17
|
+
'workflow.advocate': { type: 'boolean', default: true, category: 'Workflow', description: "Enable Devil's Advocate on plans" },
|
|
18
|
+
'workflow.auto_recover': { type: 'boolean', default: false, category: 'Workflow', description: 'Auto-recovery on verification failure' },
|
|
19
|
+
'agents.model': { type: 'string', default: 'inherit', category: 'Models', description: 'Default model for all agents' },
|
|
20
|
+
'agents.profile': { type: 'enum', values: ['quality', 'balanced', 'budget'], default: 'quality', category: 'Models', description: 'Active model profile preset' },
|
|
21
|
+
'enforcement.level': { type: 'enum', values: ['hard', 'balanced', 'soft'], default: 'hard', category: 'Enforcement', description: 'Skill enforcement strictness' },
|
|
22
|
+
'enforcement.business_paths': { type: 'string[]', default: ['commands/*.cjs', 'lib/*.cjs'], category: 'Enforcement', description: 'Glob patterns for business logic paths (TDD mandatory in balanced)', local: true },
|
|
23
|
+
'enforcement.non_business_paths': { type: 'string[]', default: ['templates/*.md', 'SKILL.md', 'hooks/*.sh', '.planning/**', '*.json'], category: 'Enforcement', description: 'Glob patterns for non-business paths (TDD optional in balanced)', local: true },
|
|
24
|
+
'monitoring.enabled': { type: 'boolean', default: true, category: 'Monitoring', description: 'Enable context monitoring' },
|
|
25
|
+
'monitoring.warning_threshold': { type: 'number', default: 35, min: 10, max: 50, category: 'Monitoring', description: 'Context warning threshold (%)' },
|
|
26
|
+
'monitoring.critical_threshold': { type: 'number', default: 25, min: 5, max: 40, category: 'Monitoring', description: 'Context critical threshold (%)' },
|
|
27
|
+
'complexity.default_budget': { type: 'number', default: 60, min: 20, max: 100, category: 'Complexity', description: 'Default complexity budget score' },
|
|
28
|
+
'storm.port': { type: 'number', default: 3456, min: 1024, max: 65535, category: 'Storm', description: 'Brainstorm server port', local: true },
|
|
29
|
+
'storm.auto_open': { type: 'boolean', default: true, category: 'Storm', description: 'Auto-open browser on storm start', local: true },
|
|
30
|
+
'adr.auto_create': { type: 'boolean', default: true, category: 'ADR', description: 'Auto-create ADRs for significant decisions' },
|
|
31
|
+
'adr.status_lifecycle': { type: 'boolean', default: true, category: 'ADR', description: 'Enable ADR status transitions' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PROFILES = {
|
|
35
|
+
quality: {
|
|
36
|
+
description: 'All agents use highest quality model',
|
|
37
|
+
models: {}
|
|
38
|
+
},
|
|
39
|
+
balanced: {
|
|
40
|
+
description: 'Quality for planning, efficient for checking',
|
|
41
|
+
models: {
|
|
42
|
+
'plan-checker': 'claude-sonnet-4-20250514',
|
|
43
|
+
verifier: 'claude-sonnet-4-20250514',
|
|
44
|
+
researcher: 'claude-sonnet-4-20250514'
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
budget: {
|
|
48
|
+
description: 'Minimize token cost',
|
|
49
|
+
models: {
|
|
50
|
+
executor: 'claude-sonnet-4-20250514',
|
|
51
|
+
researcher: 'claude-haiku-4-20250514',
|
|
52
|
+
'plan-checker': 'claude-haiku-4-20250514',
|
|
53
|
+
verifier: 'claude-haiku-4-20250514',
|
|
54
|
+
debugger: 'claude-haiku-4-20250514',
|
|
55
|
+
mapper: 'claude-haiku-4-20250514'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const MODE_PRESETS = {
|
|
61
|
+
yolo: {
|
|
62
|
+
'workflow.advocate': false,
|
|
63
|
+
'workflow.auto_recover': true,
|
|
64
|
+
'workflow.parallelization': true,
|
|
65
|
+
'enforcement.level': 'soft'
|
|
66
|
+
},
|
|
67
|
+
interactive: {
|
|
68
|
+
'workflow.advocate': true,
|
|
69
|
+
'workflow.auto_recover': false,
|
|
70
|
+
'workflow.parallelization': false,
|
|
71
|
+
'enforcement.level': 'hard'
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const ENFORCEMENT_GRADUATED = {
|
|
76
|
+
scaffolding: { hard: 'balanced', balanced: 'soft', soft: 'soft' },
|
|
77
|
+
standard: { hard: 'hard', balanced: 'balanced', soft: 'soft' },
|
|
78
|
+
production: { soft: 'balanced', balanced: 'hard', hard: 'hard' }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const LOCAL_FIELDS = ['storm.port', 'storm.auto_open', 'enforcement.business_paths', 'enforcement.non_business_paths'];
|
|
82
|
+
|
|
83
|
+
// ─── Utility Functions ──────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function getNestedValue(obj, dotPath) {
|
|
86
|
+
return dotPath.split('.').reduce((o, k) => (o != null ? o[k] : undefined), obj);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setNestedValue(obj, dotPath, value) {
|
|
90
|
+
const result = deepMerge({}, obj);
|
|
91
|
+
const keys = dotPath.split('.');
|
|
92
|
+
const last = keys.pop();
|
|
93
|
+
let parent = result;
|
|
94
|
+
for (const k of keys) {
|
|
95
|
+
if (!parent[k] || typeof parent[k] !== 'object') parent[k] = {};
|
|
96
|
+
parent = parent[k];
|
|
97
|
+
}
|
|
98
|
+
parent[last] = value;
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function deepMerge(target, source) {
|
|
103
|
+
const result = { ...target };
|
|
104
|
+
for (const key of Object.keys(source)) {
|
|
105
|
+
if (
|
|
106
|
+
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
|
|
107
|
+
result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])
|
|
108
|
+
) {
|
|
109
|
+
result[key] = deepMerge(result[key], source[key]);
|
|
110
|
+
} else {
|
|
111
|
+
result[key] = source[key];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function levenshtein(a, b) {
|
|
118
|
+
const m = a.length, n = b.length;
|
|
119
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => {
|
|
120
|
+
const row = new Array(n + 1);
|
|
121
|
+
row[0] = i;
|
|
122
|
+
return row;
|
|
123
|
+
});
|
|
124
|
+
for (let j = 1; j <= n; j++) dp[0][j] = j;
|
|
125
|
+
for (let i = 1; i <= m; i++) {
|
|
126
|
+
for (let j = 1; j <= n; j++) {
|
|
127
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
128
|
+
? dp[i - 1][j - 1]
|
|
129
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return dp[m][n];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function suggestKey(input, schemaKeys) {
|
|
136
|
+
let best = null, bestDist = Infinity;
|
|
137
|
+
for (const key of schemaKeys) {
|
|
138
|
+
const d = levenshtein(input.toLowerCase(), key.toLowerCase());
|
|
139
|
+
if (d < bestDist && d <= 3) { best = key; bestDist = d; }
|
|
140
|
+
}
|
|
141
|
+
return best;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function coerceValue(value, schemaEntry) {
|
|
145
|
+
if (value === undefined || value === null) return value;
|
|
146
|
+
|
|
147
|
+
switch (schemaEntry.type) {
|
|
148
|
+
case 'boolean':
|
|
149
|
+
if (typeof value === 'string') {
|
|
150
|
+
if (value === 'true') return true;
|
|
151
|
+
if (value === 'false') return false;
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
case 'number':
|
|
155
|
+
if (typeof value === 'string') {
|
|
156
|
+
const n = parseInt(value, 10);
|
|
157
|
+
if (!isNaN(n)) return n;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
case 'string[]':
|
|
161
|
+
if (typeof value === 'string') {
|
|
162
|
+
return value.split(',').map(s => s.trim());
|
|
163
|
+
}
|
|
164
|
+
return value;
|
|
165
|
+
default:
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── Global Defaults Path ───────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
function getGlobalDefaultsPath() {
|
|
173
|
+
return path.join(os.homedir(), '.brain', 'defaults.json');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readGlobalDefaults() {
|
|
177
|
+
const p = getGlobalDefaultsPath();
|
|
178
|
+
if (!fs.existsSync(p)) return {};
|
|
179
|
+
try {
|
|
180
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
181
|
+
} catch {
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function writeGlobalDefaults(data) {
|
|
187
|
+
const dir = path.dirname(getGlobalDefaultsPath());
|
|
188
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
189
|
+
atomicWriteSync(getGlobalDefaultsPath(), JSON.stringify(data, null, 2));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ─── Core Operations ────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
function validateConfig(key, value) {
|
|
195
|
+
const schemaKeys = Object.keys(SCHEMA);
|
|
196
|
+
|
|
197
|
+
if (!SCHEMA[key]) {
|
|
198
|
+
const suggestion = suggestKey(key, schemaKeys);
|
|
199
|
+
return {
|
|
200
|
+
valid: false,
|
|
201
|
+
error: `Unknown config key: "${key}"`,
|
|
202
|
+
suggestion: suggestion || undefined
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const entry = SCHEMA[key];
|
|
207
|
+
const coerced = coerceValue(value, entry);
|
|
208
|
+
|
|
209
|
+
switch (entry.type) {
|
|
210
|
+
case 'boolean':
|
|
211
|
+
if (typeof coerced !== 'boolean') {
|
|
212
|
+
return { valid: false, error: `"${key}" expects boolean (true/false), got: ${typeof value}` };
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
case 'enum':
|
|
216
|
+
if (!entry.values.includes(coerced)) {
|
|
217
|
+
return { valid: false, error: `"${key}" must be one of: ${entry.values.join(', ')}. Got: "${coerced}"` };
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
case 'number':
|
|
221
|
+
if (typeof coerced !== 'number' || isNaN(coerced)) {
|
|
222
|
+
return { valid: false, error: `"${key}" expects a number, got: "${value}"` };
|
|
223
|
+
}
|
|
224
|
+
if (entry.min !== undefined && coerced < entry.min) {
|
|
225
|
+
return { valid: false, error: `"${key}" minimum is ${entry.min}, got: ${coerced}` };
|
|
226
|
+
}
|
|
227
|
+
if (entry.max !== undefined && coerced > entry.max) {
|
|
228
|
+
return { valid: false, error: `"${key}" maximum is ${entry.max}, got: ${coerced}` };
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
case 'string':
|
|
232
|
+
// Strings are always valid
|
|
233
|
+
break;
|
|
234
|
+
case 'string[]':
|
|
235
|
+
if (!Array.isArray(coerced)) {
|
|
236
|
+
return { valid: false, error: `"${key}" expects an array of strings` };
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { valid: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function getConfig(brainDir, key, opts = {}) {
|
|
245
|
+
if (opts.global) {
|
|
246
|
+
const defaults = readGlobalDefaults();
|
|
247
|
+
if (!key) return defaults;
|
|
248
|
+
return getNestedValue(defaults, key) ?? defaults[key];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const state = readState(brainDir);
|
|
252
|
+
if (!state) return undefined;
|
|
253
|
+
|
|
254
|
+
if (!key) return state;
|
|
255
|
+
|
|
256
|
+
// Try dot-path on the state object
|
|
257
|
+
return getNestedValue(state, key);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function setConfig(brainDir, key, value, opts = {}) {
|
|
261
|
+
// Check if key is 'mode' -- apply preset
|
|
262
|
+
if (key === 'mode' && MODE_PRESETS[value]) {
|
|
263
|
+
const state = readState(brainDir);
|
|
264
|
+
if (!state) throw new Error('No brain.json found');
|
|
265
|
+
|
|
266
|
+
let updated = state;
|
|
267
|
+
updated.mode = value;
|
|
268
|
+
for (const [presetKey, presetVal] of Object.entries(MODE_PRESETS[value])) {
|
|
269
|
+
updated = setNestedValue(updated, presetKey, presetVal);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (opts.global) {
|
|
273
|
+
writeGlobalDefaults(updated);
|
|
274
|
+
} else {
|
|
275
|
+
writeState(brainDir, updated);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Validate key
|
|
281
|
+
const validation = validateConfig(key, value);
|
|
282
|
+
if (!validation.valid) {
|
|
283
|
+
let msg = validation.error;
|
|
284
|
+
if (validation.suggestion) {
|
|
285
|
+
msg += `. Did you mean: "${validation.suggestion}"?`;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(msg);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const entry = SCHEMA[key];
|
|
291
|
+
const coerced = coerceValue(value, entry);
|
|
292
|
+
|
|
293
|
+
if (opts.global) {
|
|
294
|
+
const defaults = readGlobalDefaults();
|
|
295
|
+
const updated = { ...defaults, [key]: coerced };
|
|
296
|
+
writeGlobalDefaults(updated);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const state = readState(brainDir);
|
|
301
|
+
if (!state) throw new Error('No brain.json found');
|
|
302
|
+
|
|
303
|
+
const updated = setNestedValue(state, key, coerced);
|
|
304
|
+
writeState(brainDir, updated);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function listConfig(brainDir, opts = {}) {
|
|
308
|
+
const state = readState(brainDir);
|
|
309
|
+
const result = {};
|
|
310
|
+
|
|
311
|
+
for (const [key, entry] of Object.entries(SCHEMA)) {
|
|
312
|
+
if (opts.category && entry.category !== opts.category) continue;
|
|
313
|
+
|
|
314
|
+
if (!result[entry.category]) result[entry.category] = [];
|
|
315
|
+
|
|
316
|
+
const value = state ? getNestedValue(state, key) : entry.default;
|
|
317
|
+
|
|
318
|
+
result[entry.category].push({
|
|
319
|
+
key,
|
|
320
|
+
value: value !== undefined ? value : entry.default,
|
|
321
|
+
default: entry.default,
|
|
322
|
+
description: entry.description
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function resetConfig(brainDir, keyOrOpts) {
|
|
330
|
+
const state = readState(brainDir);
|
|
331
|
+
if (!state) throw new Error('No brain.json found');
|
|
332
|
+
|
|
333
|
+
let updated = state;
|
|
334
|
+
|
|
335
|
+
if (typeof keyOrOpts === 'string') {
|
|
336
|
+
// Reset single key
|
|
337
|
+
const entry = SCHEMA[keyOrOpts];
|
|
338
|
+
if (!entry) throw new Error(`Unknown config key: "${keyOrOpts}"`);
|
|
339
|
+
updated = setNestedValue(updated, keyOrOpts, entry.default);
|
|
340
|
+
} else if (keyOrOpts && keyOrOpts.category) {
|
|
341
|
+
// Reset all keys in category
|
|
342
|
+
for (const [key, entry] of Object.entries(SCHEMA)) {
|
|
343
|
+
if (entry.category === keyOrOpts.category) {
|
|
344
|
+
updated = setNestedValue(updated, key, entry.default);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
// Reset all keys
|
|
349
|
+
for (const [key, entry] of Object.entries(SCHEMA)) {
|
|
350
|
+
updated = setNestedValue(updated, key, entry.default);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
writeState(brainDir, updated);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function exportConfig(brainDir) {
|
|
358
|
+
const state = readState(brainDir);
|
|
359
|
+
if (!state) throw new Error('No brain.json found');
|
|
360
|
+
|
|
361
|
+
const exported = {};
|
|
362
|
+
for (const [key, entry] of Object.entries(SCHEMA)) {
|
|
363
|
+
if (LOCAL_FIELDS.includes(key)) continue;
|
|
364
|
+
const value = getNestedValue(state, key);
|
|
365
|
+
if (value !== undefined) {
|
|
366
|
+
exported[key] = value;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return JSON.stringify(exported, null, 2);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function importConfig(brainDir, jsonString) {
|
|
374
|
+
const data = JSON.parse(jsonString);
|
|
375
|
+
let imported = 0;
|
|
376
|
+
const skipped = [];
|
|
377
|
+
|
|
378
|
+
for (const [key, value] of Object.entries(data)) {
|
|
379
|
+
const validation = validateConfig(key, value);
|
|
380
|
+
if (!validation.valid) {
|
|
381
|
+
skipped.push({ key, reason: validation.error });
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
setConfig(brainDir, key, value);
|
|
387
|
+
imported++;
|
|
388
|
+
} catch (err) {
|
|
389
|
+
skipped.push({ key, reason: err.message });
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { imported, skipped };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function mergeWithDefaults(brainDir) {
|
|
397
|
+
// Level 1: built-in defaults from SCHEMA
|
|
398
|
+
const builtIn = {};
|
|
399
|
+
for (const [key, entry] of Object.entries(SCHEMA)) {
|
|
400
|
+
builtIn[key] = entry.default;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Level 2: global defaults from ~/.brain/defaults.json
|
|
404
|
+
const global = readGlobalDefaults();
|
|
405
|
+
|
|
406
|
+
// Level 3: project config from brain.json (flat keys extracted from nested state)
|
|
407
|
+
const state = readState(brainDir);
|
|
408
|
+
const project = {};
|
|
409
|
+
if (state) {
|
|
410
|
+
for (const key of Object.keys(SCHEMA)) {
|
|
411
|
+
const val = getNestedValue(state, key);
|
|
412
|
+
if (val !== undefined) {
|
|
413
|
+
project[key] = val;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Merge: built-in -> global -> project
|
|
419
|
+
return deepMerge(deepMerge(builtIn, global), project);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function getEffectiveEnforcement(configLevel, phaseType) {
|
|
423
|
+
const table = ENFORCEMENT_GRADUATED[phaseType] || ENFORCEMENT_GRADUATED.standard;
|
|
424
|
+
return table[configLevel] || configLevel;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getSchema() {
|
|
428
|
+
return SCHEMA;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Module Exports ─────────────────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
CATEGORIES,
|
|
435
|
+
SCHEMA,
|
|
436
|
+
PROFILES,
|
|
437
|
+
MODE_PRESETS,
|
|
438
|
+
ENFORCEMENT_GRADUATED,
|
|
439
|
+
LOCAL_FIELDS,
|
|
440
|
+
getConfig,
|
|
441
|
+
setConfig,
|
|
442
|
+
listConfig,
|
|
443
|
+
resetConfig,
|
|
444
|
+
validateConfig,
|
|
445
|
+
exportConfig,
|
|
446
|
+
importConfig,
|
|
447
|
+
mergeWithDefaults,
|
|
448
|
+
deepMerge,
|
|
449
|
+
suggestKey,
|
|
450
|
+
getEffectiveEnforcement,
|
|
451
|
+
getSchema
|
|
452
|
+
};
|
package/bin/lib/core.cjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { styleText } = require('node:util');
|
|
4
|
+
|
|
5
|
+
const isTTY = !!process.stdout.isTTY;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Output data in appropriate format for the current terminal.
|
|
9
|
+
* TTY: human-readable text. Pipe/non-TTY: JSON.
|
|
10
|
+
* @param {*} data - Structured data to output
|
|
11
|
+
* @param {string} [humanFormat] - Human-readable string for TTY output
|
|
12
|
+
*/
|
|
13
|
+
function output(data, humanFormat) {
|
|
14
|
+
if (isTTY) {
|
|
15
|
+
console.log(humanFormat || formatHuman(data));
|
|
16
|
+
} else {
|
|
17
|
+
console.log(JSON.stringify(data));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default human formatting for data objects.
|
|
23
|
+
* @param {*} data
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function formatHuman(data) {
|
|
27
|
+
if (typeof data === 'string') return data;
|
|
28
|
+
return Object.entries(data)
|
|
29
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
30
|
+
.join('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Prefix a message with the [brain] tag.
|
|
35
|
+
* Uses cyan coloring on TTY.
|
|
36
|
+
* @param {string} msg
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
function prefix(msg) {
|
|
40
|
+
const tag = isTTY ? styleText('cyan', '[brain]') : '[brain]';
|
|
41
|
+
return `${tag} ${msg}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write an error message to stderr with red [brain] prefix.
|
|
46
|
+
* @param {string} msg
|
|
47
|
+
*/
|
|
48
|
+
function error(msg) {
|
|
49
|
+
const tag = isTTY ? styleText('red', '[brain]') : '[brain]';
|
|
50
|
+
console.error(`${tag} ${msg}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Write a success message to stdout with green [brain] prefix.
|
|
55
|
+
* @param {string} msg
|
|
56
|
+
*/
|
|
57
|
+
function success(msg) {
|
|
58
|
+
const tag = isTTY ? styleText('green', '[brain]') : '[brain]';
|
|
59
|
+
console.log(`${tag} ${msg}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { isTTY, output, prefix, error, success };
|