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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/agents/brain-checker.md +33 -0
  4. package/agents/brain-debugger.md +35 -0
  5. package/agents/brain-executor.md +37 -0
  6. package/agents/brain-mapper.md +44 -0
  7. package/agents/brain-planner.md +49 -0
  8. package/agents/brain-researcher.md +47 -0
  9. package/agents/brain-synthesizer.md +43 -0
  10. package/agents/brain-verifier.md +41 -0
  11. package/bin/brain-tools.cjs +185 -0
  12. package/bin/lib/adr.cjs +283 -0
  13. package/bin/lib/agents.cjs +152 -0
  14. package/bin/lib/anti-patterns.cjs +183 -0
  15. package/bin/lib/audit.cjs +268 -0
  16. package/bin/lib/commands/adr.cjs +126 -0
  17. package/bin/lib/commands/complete.cjs +270 -0
  18. package/bin/lib/commands/config.cjs +306 -0
  19. package/bin/lib/commands/discuss.cjs +237 -0
  20. package/bin/lib/commands/execute.cjs +415 -0
  21. package/bin/lib/commands/health.cjs +103 -0
  22. package/bin/lib/commands/map.cjs +101 -0
  23. package/bin/lib/commands/new-project.cjs +885 -0
  24. package/bin/lib/commands/pause.cjs +142 -0
  25. package/bin/lib/commands/phase-manage.cjs +357 -0
  26. package/bin/lib/commands/plan.cjs +451 -0
  27. package/bin/lib/commands/progress.cjs +167 -0
  28. package/bin/lib/commands/quick.cjs +447 -0
  29. package/bin/lib/commands/resume.cjs +196 -0
  30. package/bin/lib/commands/storm.cjs +590 -0
  31. package/bin/lib/commands/verify.cjs +504 -0
  32. package/bin/lib/commands.cjs +263 -0
  33. package/bin/lib/complexity.cjs +138 -0
  34. package/bin/lib/complexity.test.cjs +108 -0
  35. package/bin/lib/config.cjs +452 -0
  36. package/bin/lib/core.cjs +62 -0
  37. package/bin/lib/detect.cjs +603 -0
  38. package/bin/lib/git.cjs +112 -0
  39. package/bin/lib/health.cjs +356 -0
  40. package/bin/lib/init.cjs +310 -0
  41. package/bin/lib/logger.cjs +100 -0
  42. package/bin/lib/platform.cjs +58 -0
  43. package/bin/lib/requirements.cjs +158 -0
  44. package/bin/lib/roadmap.cjs +228 -0
  45. package/bin/lib/security.cjs +237 -0
  46. package/bin/lib/state.cjs +353 -0
  47. package/bin/lib/templates.cjs +48 -0
  48. package/bin/templates/advocate.md +182 -0
  49. package/bin/templates/checkpoint.md +55 -0
  50. package/bin/templates/debugger.md +148 -0
  51. package/bin/templates/discuss.md +60 -0
  52. package/bin/templates/executor.md +201 -0
  53. package/bin/templates/mapper.md +129 -0
  54. package/bin/templates/plan-checker.md +134 -0
  55. package/bin/templates/planner.md +165 -0
  56. package/bin/templates/researcher.md +78 -0
  57. package/bin/templates/storm.html +376 -0
  58. package/bin/templates/synthesis.md +30 -0
  59. package/bin/templates/verifier.md +181 -0
  60. package/commands/brain/adr.md +34 -0
  61. package/commands/brain/complete.md +37 -0
  62. package/commands/brain/config.md +37 -0
  63. package/commands/brain/discuss.md +35 -0
  64. package/commands/brain/execute.md +38 -0
  65. package/commands/brain/health.md +33 -0
  66. package/commands/brain/map.md +35 -0
  67. package/commands/brain/new-project.md +38 -0
  68. package/commands/brain/pause.md +26 -0
  69. package/commands/brain/plan.md +38 -0
  70. package/commands/brain/progress.md +28 -0
  71. package/commands/brain/quick.md +51 -0
  72. package/commands/brain/resume.md +28 -0
  73. package/commands/brain/storm.md +30 -0
  74. package/commands/brain/verify.md +39 -0
  75. package/hooks/bootstrap.sh +54 -0
  76. package/hooks/post-tool-use.sh +45 -0
  77. package/hooks/statusline.sh +130 -0
  78. 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
+ };
@@ -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 };