brains-cli 0.1.0 → 1.0.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.
- package/.env.example +2 -0
- package/README.md +64 -81
- package/bin/brains.js +23 -1
- package/package.json +3 -1
- package/src/api.js +115 -0
- package/src/commands/config.js +236 -0
- package/src/commands/install.js +3 -1
- package/src/commands/run.js +477 -317
- package/src/config.js +237 -0
- package/src/providers/anthropic.js +83 -0
- package/src/providers/openai-compat.js +96 -0
- package/src/providers/registry.js +125 -0
- package/src/utils/files.js +239 -0
package/src/commands/run.js
CHANGED
|
@@ -5,8 +5,17 @@ const chalk = require('chalk');
|
|
|
5
5
|
const ora = require('ora');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs');
|
|
8
|
+
const readline = require('readline');
|
|
8
9
|
const { getBrainById } = require('../registry');
|
|
9
|
-
const {
|
|
10
|
+
const {
|
|
11
|
+
getActiveProvider, getProviderApiKey, getProviderModel,
|
|
12
|
+
setProviderApiKey, setActiveProvider,
|
|
13
|
+
getModel, saveSession,
|
|
14
|
+
} = require('../config');
|
|
15
|
+
const { getProviderDef, getAllProviderNames } = require('../providers/registry');
|
|
16
|
+
const { streamMessage } = require('../api');
|
|
17
|
+
const { readFilesForReview, parseFileBlocks, writeFileBlocks, generateTreeView } = require('../utils/files');
|
|
18
|
+
const { showSuccess, showError, showInfo, showWarning, ACCENT, DIM, SUCCESS, WARN } = require('../utils/ui');
|
|
10
19
|
|
|
11
20
|
const BRAINS_DIR = path.join(require('os').homedir(), '.brains');
|
|
12
21
|
const INSTALLED_FILE = path.join(BRAINS_DIR, 'installed.json');
|
|
@@ -28,6 +37,138 @@ function loadBrainConfig(brainId) {
|
|
|
28
37
|
return null;
|
|
29
38
|
}
|
|
30
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Ensure API key is available for the active provider.
|
|
42
|
+
* Prompts user if missing. Offers provider switching if no key.
|
|
43
|
+
*/
|
|
44
|
+
async function ensureApiKey() {
|
|
45
|
+
const providerName = getActiveProvider();
|
|
46
|
+
const providerDef = getProviderDef(providerName);
|
|
47
|
+
|
|
48
|
+
// Ollama doesn't need a key
|
|
49
|
+
if (providerDef && providerDef.requires_key === false) {
|
|
50
|
+
return 'not-required';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let key = getProviderApiKey(providerName);
|
|
54
|
+
if (key) return key;
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
showWarning(`No API key found for ${chalk.bold(providerDef ? providerDef.name : providerName)}.`);
|
|
58
|
+
|
|
59
|
+
if (providerDef && providerDef.key_url) {
|
|
60
|
+
showInfo(`Get one at ${ACCENT(providerDef.key_url)}\n`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const keyHint = providerDef && providerDef.key_prefix
|
|
64
|
+
? `Key should start with ${providerDef.key_prefix}`
|
|
65
|
+
: '';
|
|
66
|
+
|
|
67
|
+
const { apiKey } = await inquirer.prompt([
|
|
68
|
+
{
|
|
69
|
+
type: 'password',
|
|
70
|
+
name: 'apiKey',
|
|
71
|
+
message: `Enter your ${providerDef ? providerDef.name : providerName} API key:`,
|
|
72
|
+
mask: '*',
|
|
73
|
+
validate: (v) => {
|
|
74
|
+
if (!v || v.length < 5) return 'Please enter a valid API key';
|
|
75
|
+
if (providerDef && providerDef.key_prefix && !v.startsWith(providerDef.key_prefix)) {
|
|
76
|
+
return keyHint;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
const { save } = await inquirer.prompt([
|
|
84
|
+
{
|
|
85
|
+
type: 'confirm',
|
|
86
|
+
name: 'save',
|
|
87
|
+
message: 'Save this key to ~/.brains/config.json for future use?',
|
|
88
|
+
default: true,
|
|
89
|
+
},
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
if (save) {
|
|
93
|
+
setProviderApiKey(providerName, apiKey);
|
|
94
|
+
showSuccess('API key saved.');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set in environment for this session
|
|
98
|
+
if (providerDef && providerDef.key_env) {
|
|
99
|
+
process.env[providerDef.key_env] = apiKey;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return apiKey;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build a merged system prompt when stacking brains.
|
|
107
|
+
*/
|
|
108
|
+
function buildSystemPrompt(brain, stackedBrains) {
|
|
109
|
+
let prompt = brain.systemPrompt;
|
|
110
|
+
|
|
111
|
+
if (stackedBrains.length > 0) {
|
|
112
|
+
prompt += '\n\n--- ADDITIONAL EXPERTISE ---\n';
|
|
113
|
+
prompt += 'You also have the following additional expertise available. ';
|
|
114
|
+
prompt += 'Incorporate this knowledge when relevant:\n\n';
|
|
115
|
+
|
|
116
|
+
for (const extra of stackedBrains) {
|
|
117
|
+
prompt += `## ${extra.name} (${extra.category})\n`;
|
|
118
|
+
prompt += extra.systemPrompt + '\n\n';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return prompt;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Format user answers as context for Claude.
|
|
127
|
+
*/
|
|
128
|
+
function formatContext(answers, brain) {
|
|
129
|
+
let context = `The user provided the following context:\n\n`;
|
|
130
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
131
|
+
if (value && value.trim()) {
|
|
132
|
+
context += `**${key}**: ${value}\n`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return context;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── Category color map ───
|
|
139
|
+
const CATEGORY_COLORS = {
|
|
140
|
+
builder: '#FF3366',
|
|
141
|
+
role: '#7B2FFF',
|
|
142
|
+
reviewer: '#00AAFF',
|
|
143
|
+
domain: '#00CC88',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
function showSessionHeader(brain, stackedBrains) {
|
|
147
|
+
const color = CATEGORY_COLORS[brain.category] || '#ffffff';
|
|
148
|
+
|
|
149
|
+
console.log('');
|
|
150
|
+
console.log(chalk.hex(color).bold(' ┌─────────────────────────────────────────────────────'));
|
|
151
|
+
console.log(
|
|
152
|
+
chalk.hex(color).bold(` │ 🧠 ${brain.name}`) + DIM(` — ${brain.category}`)
|
|
153
|
+
);
|
|
154
|
+
if (stackedBrains.length > 0) {
|
|
155
|
+
console.log(
|
|
156
|
+
chalk.hex(color).bold(' │ ') + DIM(`+ ${stackedBrains.map((b) => b.name).join(', ')}`)
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const providerName = getActiveProvider();
|
|
160
|
+
const providerDef = getProviderDef(providerName);
|
|
161
|
+
const providerLabel = providerDef ? providerDef.name : providerName;
|
|
162
|
+
|
|
163
|
+
console.log(chalk.hex(color).bold(' │ ') + DIM(`Stack: ${brain.stack.join(', ')}`));
|
|
164
|
+
console.log(chalk.hex(color).bold(' │ ') + DIM(`Provider: ${providerLabel} • Model: ${getModel()}`));
|
|
165
|
+
console.log(chalk.hex(color).bold(' └─────────────────────────────────────────────────────'));
|
|
166
|
+
console.log('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ═══════════════════════════════════════════
|
|
170
|
+
// MAIN RUN FUNCTION
|
|
171
|
+
// ═══════════════════════════════════════════
|
|
31
172
|
async function run(brainName, options) {
|
|
32
173
|
const brain = getBrainById(brainName);
|
|
33
174
|
|
|
@@ -57,6 +198,9 @@ async function run(brainName, options) {
|
|
|
57
198
|
}
|
|
58
199
|
}
|
|
59
200
|
|
|
201
|
+
// Ensure API key
|
|
202
|
+
await ensureApiKey();
|
|
203
|
+
|
|
60
204
|
const config = loadBrainConfig(brainName) || brain;
|
|
61
205
|
|
|
62
206
|
// Handle brain stacking
|
|
@@ -71,82 +215,36 @@ async function run(brainName, options) {
|
|
|
71
215
|
stackedBrains.push(extra);
|
|
72
216
|
showInfo(` ${SUCCESS('+')} ${extra.name} loaded`);
|
|
73
217
|
} else {
|
|
74
|
-
|
|
218
|
+
showWarning(` ${extraBrain} not found, skipping`);
|
|
75
219
|
}
|
|
76
220
|
}
|
|
77
221
|
}
|
|
78
222
|
|
|
79
|
-
// Show brain activation
|
|
80
|
-
console.log('');
|
|
81
|
-
const spinner = ora({
|
|
82
|
-
text: `Activating ${chalk.bold(brain.name)}...`,
|
|
83
|
-
color: 'magenta',
|
|
84
|
-
spinner: 'dots12',
|
|
85
|
-
}).start();
|
|
86
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
87
|
-
spinner.succeed(`${chalk.bold(brain.name)} is active`);
|
|
88
|
-
|
|
89
|
-
if (stackedBrains.length > 0) {
|
|
90
|
-
const stackSpinner = ora({
|
|
91
|
-
text: 'Merging stacked brain contexts...',
|
|
92
|
-
color: 'cyan',
|
|
93
|
-
spinner: 'dots12',
|
|
94
|
-
}).start();
|
|
95
|
-
await new Promise((r) => setTimeout(r, 800));
|
|
96
|
-
stackSpinner.succeed('Brain stack ready');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
223
|
// Show session header
|
|
100
|
-
|
|
101
|
-
builder: '#FF3366',
|
|
102
|
-
role: '#7B2FFF',
|
|
103
|
-
reviewer: '#00AAFF',
|
|
104
|
-
domain: '#00CC88',
|
|
105
|
-
};
|
|
106
|
-
const color = categoryColors[brain.category] || '#ffffff';
|
|
107
|
-
|
|
108
|
-
console.log('');
|
|
109
|
-
console.log(
|
|
110
|
-
chalk.hex(color).bold(' ┌─────────────────────────────────────────────────────')
|
|
111
|
-
);
|
|
112
|
-
console.log(
|
|
113
|
-
chalk.hex(color).bold(` │ 🧠 ${brain.name}`) +
|
|
114
|
-
DIM(` — ${brain.category}`)
|
|
115
|
-
);
|
|
116
|
-
if (stackedBrains.length > 0) {
|
|
117
|
-
console.log(
|
|
118
|
-
chalk.hex(color).bold(' │ ') +
|
|
119
|
-
DIM(`+ ${stackedBrains.map((b) => b.name).join(', ')}`)
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
console.log(
|
|
123
|
-
chalk.hex(color).bold(' │ ') + DIM(`Stack: ${brain.stack.join(', ')}`)
|
|
124
|
-
);
|
|
125
|
-
console.log(
|
|
126
|
-
chalk.hex(color).bold(' └─────────────────────────────────────────────────────')
|
|
127
|
-
);
|
|
128
|
-
console.log('');
|
|
224
|
+
showSessionHeader(brain, stackedBrains);
|
|
129
225
|
|
|
130
|
-
// ───
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
226
|
+
// ─── Route to brain-specific workflow ───
|
|
227
|
+
try {
|
|
228
|
+
if (brain.category === 'builder') {
|
|
229
|
+
await runBuilderBrain(brain, config, stackedBrains, options);
|
|
230
|
+
} else if (brain.category === 'reviewer') {
|
|
231
|
+
await runReviewerBrain(brain, config, stackedBrains, options);
|
|
232
|
+
} else if (brain.category === 'role' || brain.category === 'domain') {
|
|
233
|
+
await runConversationalBrain(brain, config, stackedBrains, options);
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
handleApiError(err);
|
|
140
237
|
}
|
|
141
238
|
}
|
|
142
239
|
|
|
143
|
-
//
|
|
144
|
-
|
|
240
|
+
// ═══════════════════════════════════════════
|
|
241
|
+
// BUILDER BRAIN — Ask questions → Generate → Write files
|
|
242
|
+
// ═══════════════════════════════════════════
|
|
243
|
+
async function runBuilderBrain(brain, config, stackedBrains, options) {
|
|
145
244
|
showInfo(`${brain.name} will ask you a few questions, then generate your project.\n`);
|
|
146
245
|
|
|
246
|
+
// Run interview
|
|
147
247
|
const answers = {};
|
|
148
|
-
|
|
149
|
-
// Run through the brain's interview questions
|
|
150
248
|
for (const q of config.questions || brain.questions) {
|
|
151
249
|
const { answer } = await inquirer.prompt([
|
|
152
250
|
{
|
|
@@ -158,61 +256,111 @@ async function runBuilderBrain(brain, config, options) {
|
|
|
158
256
|
answers[q.key] = answer;
|
|
159
257
|
}
|
|
160
258
|
|
|
259
|
+
const projectName = answers.product || answers.name || answers.idea || brain.id;
|
|
260
|
+
const projectDir = path.resolve(options.dir || `./${projectName.toLowerCase().replace(/\s+/g, '-')}-project`);
|
|
261
|
+
|
|
161
262
|
console.log('');
|
|
162
|
-
showInfo('
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
263
|
+
showInfo('Generating your project... (this may take a moment)\n');
|
|
264
|
+
|
|
265
|
+
const systemPrompt = buildSystemPrompt(brain, stackedBrains) + `
|
|
266
|
+
|
|
267
|
+
CRITICAL OUTPUT FORMAT:
|
|
268
|
+
You MUST output every file using this exact format so the CLI can parse and write them:
|
|
269
|
+
|
|
270
|
+
FILE: relative/path/to/file.ext
|
|
271
|
+
\`\`\`language
|
|
272
|
+
file contents here
|
|
273
|
+
\`\`\`
|
|
274
|
+
|
|
275
|
+
Output ALL files needed for a complete, working project. Include package.json, config files, and every source file.
|
|
276
|
+
Do NOT skip any files. Do NOT use placeholders like "// ... rest of code". Output complete file contents.`;
|
|
277
|
+
|
|
278
|
+
const userMessage = formatContext(answers, brain) +
|
|
279
|
+
`\n\nGenerate the complete project. Output every file using the FILE: format specified in your instructions.`;
|
|
280
|
+
|
|
281
|
+
// Stream the response — accumulate silently, then parse files
|
|
282
|
+
const spinner = ora({
|
|
283
|
+
text: 'Generating project files (streaming from Claude)...',
|
|
284
|
+
color: 'magenta',
|
|
285
|
+
spinner: 'dots12',
|
|
286
|
+
}).start();
|
|
287
|
+
|
|
288
|
+
let receivedBytes = 0;
|
|
289
|
+
let fullResponse = '';
|
|
290
|
+
try {
|
|
291
|
+
fullResponse = await streamMessage({
|
|
292
|
+
systemPrompt,
|
|
293
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
294
|
+
maxTokens: 8096,
|
|
295
|
+
onText: (text) => {
|
|
296
|
+
receivedBytes += text.length;
|
|
297
|
+
if (receivedBytes % 200 < text.length) {
|
|
298
|
+
spinner.text = `Generating... (${(receivedBytes / 1024).toFixed(0)}KB received)`;
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
onStart: () => {},
|
|
302
|
+
onEnd: () => {
|
|
303
|
+
spinner.succeed(`Generation complete (${(receivedBytes / 1024).toFixed(1)}KB)`);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
} catch (err) {
|
|
307
|
+
spinner.stop();
|
|
308
|
+
throw err;
|
|
174
309
|
}
|
|
175
310
|
|
|
176
|
-
//
|
|
177
|
-
const
|
|
311
|
+
// Parse file blocks from response
|
|
312
|
+
const fileBlocks = parseFileBlocks(fullResponse);
|
|
178
313
|
|
|
314
|
+
if (fileBlocks.length === 0) {
|
|
315
|
+
console.log('');
|
|
316
|
+
showWarning('No file blocks detected in response. Showing raw output:\n');
|
|
317
|
+
console.log(chalk.dim(fullResponse.slice(0, 2000)));
|
|
318
|
+
if (fullResponse.length > 2000) {
|
|
319
|
+
console.log(chalk.dim(`\n... (${fullResponse.length} chars total)`));
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Write files to disk
|
|
179
325
|
console.log('');
|
|
180
|
-
|
|
326
|
+
const writeSpinner = ora({
|
|
327
|
+
text: `Writing ${fileBlocks.length} files to ${projectDir}...`,
|
|
328
|
+
color: 'magenta',
|
|
329
|
+
spinner: 'dots12',
|
|
330
|
+
}).start();
|
|
181
331
|
|
|
182
|
-
const
|
|
183
|
-
|
|
332
|
+
const { written, errors } = writeFileBlocks(projectDir, fileBlocks);
|
|
333
|
+
writeSpinner.succeed(`${written.length} files written`);
|
|
184
334
|
|
|
335
|
+
if (errors.length > 0) {
|
|
336
|
+
for (const err of errors) {
|
|
337
|
+
showWarning(` Failed: ${err}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Show project tree
|
|
185
342
|
console.log('');
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log(
|
|
343
|
+
showSuccess('Project generated successfully!\n');
|
|
344
|
+
const tree = generateTreeView(written, path.basename(projectDir));
|
|
345
|
+
console.log(chalk.dim(' ' + tree.split('\n').join('\n ')));
|
|
346
|
+
|
|
347
|
+
// Show next steps
|
|
348
|
+
console.log('');
|
|
349
|
+
showInfo(`Project created at: ${ACCENT(projectDir)}`);
|
|
350
|
+
showInfo('Next steps:');
|
|
351
|
+
console.log(` ${DIM('$')} ${ACCENT(`cd ${path.basename(projectDir)}`)}`);
|
|
189
352
|
console.log(` ${DIM('$')} ${ACCENT('npm install')}`);
|
|
190
353
|
console.log(` ${DIM('$')} ${ACCENT('npm run dev')}`);
|
|
191
354
|
console.log('');
|
|
192
|
-
|
|
193
|
-
// Show what the brain generated
|
|
194
|
-
const systemPromptInfo = `
|
|
195
|
-
${chalk.bold('What was generated:')}
|
|
196
|
-
|
|
197
|
-
This project was scaffolded by the ${chalk.hex('#FF3366').bold(brain.name)} Brain.
|
|
198
|
-
The brain used the following context to customize your project:
|
|
199
|
-
${Object.entries(answers)
|
|
200
|
-
.map(([k, v]) => ` ${chalk.hex('#FF3366')('▸')} ${k}: ${DIM(v)}`)
|
|
201
|
-
.join('\n')}
|
|
202
|
-
|
|
203
|
-
${chalk.bold('Brain System Prompt:')}
|
|
204
|
-
${DIM('The brain operated with a specialized system prompt that ensures')}
|
|
205
|
-
${DIM('best practices, modern patterns, and production-grade output.')}
|
|
206
|
-
${DIM(`View it at: ~/.brains/${brain.id}/BRAIN.md`)}
|
|
207
|
-
`;
|
|
208
|
-
console.log(systemPromptInfo);
|
|
209
355
|
}
|
|
210
356
|
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
357
|
+
// ═══════════════════════════════════════════
|
|
358
|
+
// REVIEWER BRAIN — Read codebase → Analyze → Report
|
|
359
|
+
// ═══════════════════════════════════════════
|
|
360
|
+
async function runReviewerBrain(brain, config, stackedBrains, options) {
|
|
361
|
+
const targetDir = path.resolve(options.dir || process.cwd());
|
|
214
362
|
|
|
215
|
-
showInfo(`${brain.name} will analyze your codebase
|
|
363
|
+
showInfo(`${brain.name} will analyze your codebase at ${ACCENT(targetDir)}\n`);
|
|
216
364
|
|
|
217
365
|
// Ask focus questions
|
|
218
366
|
const answers = {};
|
|
@@ -227,55 +375,81 @@ async function runReviewerBrain(brain, config, options) {
|
|
|
227
375
|
answers[q.key] = answer;
|
|
228
376
|
}
|
|
229
377
|
|
|
378
|
+
// Scan files
|
|
230
379
|
console.log('');
|
|
380
|
+
const scanSpinner = ora({
|
|
381
|
+
text: 'Scanning project files...',
|
|
382
|
+
color: 'cyan',
|
|
383
|
+
spinner: 'dots12',
|
|
384
|
+
}).start();
|
|
385
|
+
|
|
386
|
+
const { content: filesContent, fileCount, totalSize, files } = readFilesForReview(targetDir);
|
|
231
387
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
{ text: 'Running pattern analysis', time: 800 },
|
|
237
|
-
{ text: 'Evaluating security posture', time: 700 },
|
|
238
|
-
{ text: 'Generating review report', time: 600 },
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
for (const step of scanSteps) {
|
|
242
|
-
const spinner = ora({
|
|
243
|
-
text: step.text,
|
|
244
|
-
color: 'cyan',
|
|
245
|
-
spinner: 'dots12',
|
|
246
|
-
}).start();
|
|
247
|
-
await new Promise((r) => setTimeout(r, step.time));
|
|
248
|
-
spinner.succeed(chalk.dim(step.text));
|
|
388
|
+
if (fileCount === 0) {
|
|
389
|
+
scanSpinner.fail('No source files found in this directory');
|
|
390
|
+
showInfo(`Make sure you're in a project directory with source code.`);
|
|
391
|
+
return;
|
|
249
392
|
}
|
|
250
393
|
|
|
394
|
+
scanSpinner.succeed(`Found ${fileCount} files (${(totalSize / 1024).toFixed(1)}KB)`);
|
|
395
|
+
|
|
396
|
+
// Show scanned files
|
|
397
|
+
console.log(chalk.dim(` Files: ${files.slice(0, 10).join(', ')}${files.length > 10 ? ` ... +${files.length - 10} more` : ''}`));
|
|
251
398
|
console.log('');
|
|
252
|
-
showSuccess('Review complete!\n');
|
|
253
399
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
console.log(` ${chalk.blue('📝 NOTE')} ${chalk.dim('2 notes')}`);
|
|
400
|
+
const reviewSpinner = ora({
|
|
401
|
+
text: 'Analyzing codebase with Claude...',
|
|
402
|
+
color: 'cyan',
|
|
403
|
+
spinner: 'dots12',
|
|
404
|
+
}).start();
|
|
260
405
|
|
|
261
|
-
|
|
406
|
+
const systemPrompt = buildSystemPrompt(brain, stackedBrains);
|
|
407
|
+
const focusArea = answers.focus || answers.type || 'general review';
|
|
262
408
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
409
|
+
const userMessage = `Review the following codebase. Focus area: ${focusArea}
|
|
410
|
+
|
|
411
|
+
${filesContent}
|
|
412
|
+
|
|
413
|
+
Provide a structured review using the severity format (🔴 CRITICAL, 🟡 WARNING, 🟢 SUGGESTION, 📝 NOTE). Be specific with file names and line references. Start with a summary, then list findings.`;
|
|
414
|
+
|
|
415
|
+
let fullResponse = '';
|
|
416
|
+
try {
|
|
417
|
+
fullResponse = await streamMessage({
|
|
418
|
+
systemPrompt,
|
|
419
|
+
messages: [{ role: 'user', content: userMessage }],
|
|
420
|
+
maxTokens: 8096,
|
|
421
|
+
onStart: () => {
|
|
422
|
+
reviewSpinner.stop();
|
|
423
|
+
console.log(chalk.bold('\n Review Report\n'));
|
|
424
|
+
console.log(chalk.dim('─'.repeat(56)) + '\n');
|
|
425
|
+
},
|
|
426
|
+
onText: (text) => process.stdout.write(text),
|
|
427
|
+
onEnd: () => {
|
|
428
|
+
console.log('\n\n' + chalk.dim('─'.repeat(56)));
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
} catch (err) {
|
|
432
|
+
reviewSpinner.stop();
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
271
435
|
|
|
272
|
-
|
|
436
|
+
// Save session
|
|
437
|
+
const sessionFile = saveSession(brain.id, [
|
|
438
|
+
{ role: 'user', content: `Review ${fileCount} files from ${targetDir}` },
|
|
439
|
+
{ role: 'assistant', content: fullResponse },
|
|
440
|
+
]);
|
|
441
|
+
console.log('');
|
|
442
|
+
showInfo(`Review saved to: ${DIM(sessionFile)}`);
|
|
273
443
|
console.log('');
|
|
274
444
|
}
|
|
275
445
|
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
446
|
+
// ═══════════════════════════════════════════
|
|
447
|
+
// CONVERSATIONAL BRAIN (Role + Domain) — REPL Loop
|
|
448
|
+
// ═══════════════════════════════════════════
|
|
449
|
+
async function runConversationalBrain(brain, config, stackedBrains, options) {
|
|
450
|
+
const color = CATEGORY_COLORS[brain.category] || '#ffffff';
|
|
451
|
+
|
|
452
|
+
showInfo(`Starting conversational session with ${brain.name}.\n`);
|
|
279
453
|
|
|
280
454
|
// Ask initial context questions
|
|
281
455
|
const answers = {};
|
|
@@ -284,204 +458,190 @@ async function runRoleBrain(brain, config, options) {
|
|
|
284
458
|
{
|
|
285
459
|
type: 'input',
|
|
286
460
|
name: 'answer',
|
|
287
|
-
message: chalk.hex(
|
|
461
|
+
message: chalk.hex(color)(q.question),
|
|
288
462
|
},
|
|
289
463
|
]);
|
|
290
464
|
answers[q.key] = answer;
|
|
291
465
|
}
|
|
292
466
|
|
|
293
|
-
|
|
467
|
+
const systemPrompt = buildSystemPrompt(brain, stackedBrains);
|
|
468
|
+
const conversationHistory = [];
|
|
294
469
|
|
|
295
|
-
|
|
296
|
-
|
|
470
|
+
// Build initial context message from answers
|
|
471
|
+
const contextMessage = formatContext(answers, brain) +
|
|
472
|
+
'\n\nPlease acknowledge this context and let me know you\'re ready. Keep it brief.';
|
|
473
|
+
|
|
474
|
+
conversationHistory.push({ role: 'user', content: contextMessage });
|
|
475
|
+
|
|
476
|
+
// Get initial response
|
|
477
|
+
console.log('');
|
|
478
|
+
const initSpinner = ora({
|
|
479
|
+
text: 'Loading context...',
|
|
297
480
|
color: 'magenta',
|
|
298
481
|
spinner: 'dots12',
|
|
299
482
|
}).start();
|
|
300
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
301
|
-
spinner.succeed(chalk.dim('Context loaded'));
|
|
302
483
|
|
|
303
|
-
|
|
484
|
+
let initialResponse = '';
|
|
485
|
+
try {
|
|
486
|
+
initialResponse = await streamMessage({
|
|
487
|
+
systemPrompt,
|
|
488
|
+
messages: conversationHistory,
|
|
489
|
+
maxTokens: 2048,
|
|
490
|
+
onStart: () => {
|
|
491
|
+
initSpinner.stop();
|
|
492
|
+
console.log(`\n ${chalk.hex(color).bold(`[${brain.name}]`)}\n`);
|
|
493
|
+
},
|
|
494
|
+
onText: (text) => process.stdout.write(text),
|
|
495
|
+
onEnd: () => console.log('\n'),
|
|
496
|
+
});
|
|
497
|
+
} catch (err) {
|
|
498
|
+
initSpinner.stop();
|
|
499
|
+
throw err;
|
|
500
|
+
}
|
|
304
501
|
|
|
305
|
-
|
|
306
|
-
` ${chalk.bold('In production, this brain would now:')}\n` +
|
|
307
|
-
` ${chalk.hex('#7B2FFF')('▸')} Enter a REPL-style conversational loop\n` +
|
|
308
|
-
` ${chalk.hex('#7B2FFF')('▸')} Use the ${brain.name} system prompt to guide every response\n` +
|
|
309
|
-
` ${chalk.hex('#7B2FFF')('▸')} Generate code, review PRs, design systems interactively\n` +
|
|
310
|
-
` ${chalk.hex('#7B2FFF')('▸')} Maintain context across the conversation\n` +
|
|
311
|
-
` ${chalk.hex('#7B2FFF')('▸')} Write files directly to your project\n`
|
|
312
|
-
);
|
|
502
|
+
conversationHistory.push({ role: 'assistant', content: initialResponse });
|
|
313
503
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
` ${DIM('Personality, methodology, tech preferences, and output format.')}\n` +
|
|
317
|
-
` ${DIM(`View it at: ~/.brains/${brain.id}/BRAIN.md`)}\n`
|
|
318
|
-
);
|
|
504
|
+
// Enter REPL loop
|
|
505
|
+
console.log(chalk.dim(' Type your message and press Enter. Commands: exit, clear, save\n'));
|
|
319
506
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
message: 'Want to see a sample interaction?',
|
|
326
|
-
default: true,
|
|
327
|
-
},
|
|
328
|
-
]);
|
|
507
|
+
const rl = readline.createInterface({
|
|
508
|
+
input: process.stdin,
|
|
509
|
+
output: process.stdout,
|
|
510
|
+
prompt: chalk.hex(color).bold(' You: '),
|
|
511
|
+
});
|
|
329
512
|
|
|
330
|
-
|
|
331
|
-
console.log(`\n ${chalk.hex('#7B2FFF').bold(`[${brain.name}]`)} ${chalk.white("I've reviewed your request. Here's my approach:")}\n`);
|
|
332
|
-
console.log(DIM(' (In production, Claude would respond here using the brain\'s system prompt'));
|
|
333
|
-
console.log(DIM(' as its personality and methodology, generating real code and advice.)\n'));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
513
|
+
rl.prompt();
|
|
336
514
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
showInfo(`${brain.name} expert session started.\n`);
|
|
515
|
+
rl.on('line', async (line) => {
|
|
516
|
+
const input = line.trim();
|
|
340
517
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
type: 'input',
|
|
346
|
-
name: 'answer',
|
|
347
|
-
message: chalk.hex('#00CC88')(q.question),
|
|
348
|
-
},
|
|
349
|
-
]);
|
|
350
|
-
}
|
|
518
|
+
if (!input) {
|
|
519
|
+
rl.prompt();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
351
522
|
|
|
352
|
-
|
|
523
|
+
// Handle commands
|
|
524
|
+
if (input === 'exit' || input === 'quit') {
|
|
525
|
+
const sessionFile = saveSession(brain.id, conversationHistory);
|
|
526
|
+
console.log('');
|
|
527
|
+
showSuccess('Session ended.');
|
|
528
|
+
showInfo(`Conversation saved to: ${DIM(sessionFile)}`);
|
|
529
|
+
console.log('');
|
|
530
|
+
rl.close();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
353
533
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
534
|
+
if (input === 'clear') {
|
|
535
|
+
conversationHistory.length = 0;
|
|
536
|
+
console.log('');
|
|
537
|
+
showInfo('Conversation history cleared.');
|
|
538
|
+
console.log('');
|
|
539
|
+
rl.prompt();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
361
542
|
|
|
362
|
-
|
|
543
|
+
if (input === 'save') {
|
|
544
|
+
const sessionFile = saveSession(brain.id, conversationHistory);
|
|
545
|
+
showInfo(`Saved to: ${DIM(sessionFile)}`);
|
|
546
|
+
rl.prompt();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
363
549
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
550
|
+
// Send to Claude
|
|
551
|
+
conversationHistory.push({ role: 'user', content: input });
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
console.log('');
|
|
555
|
+
const response = await streamMessage({
|
|
556
|
+
systemPrompt,
|
|
557
|
+
messages: conversationHistory,
|
|
558
|
+
maxTokens: 8096,
|
|
559
|
+
onStart: () => {
|
|
560
|
+
console.log(` ${chalk.hex(color).bold(`[${brain.name}]`)}\n`);
|
|
561
|
+
},
|
|
562
|
+
onText: (text) => process.stdout.write(text),
|
|
563
|
+
onEnd: () => console.log('\n'),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
conversationHistory.push({ role: 'assistant', content: response });
|
|
567
|
+
} catch (err) {
|
|
568
|
+
handleApiError(err);
|
|
569
|
+
}
|
|
372
570
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
571
|
+
rl.prompt();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
rl.on('close', () => {
|
|
575
|
+
// Ensure clean exit
|
|
576
|
+
});
|
|
376
577
|
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
{ text: 'Selecting architecture', time: 400 },
|
|
382
|
-
];
|
|
383
|
-
|
|
384
|
-
const specific = {
|
|
385
|
-
resume: [
|
|
386
|
-
{ text: 'Generating Next.js project', time: 700 },
|
|
387
|
-
{ text: 'Creating resume components', time: 600 },
|
|
388
|
-
{ text: 'Applying theme and typography', time: 500 },
|
|
389
|
-
{ text: 'Adding responsive styles', time: 400 },
|
|
390
|
-
{ text: 'Configuring SEO metadata', time: 300 },
|
|
391
|
-
{ text: 'Setting up Vercel deployment', time: 300 },
|
|
392
|
-
],
|
|
393
|
-
mvp: [
|
|
394
|
-
{ text: 'Generating Next.js + Prisma project', time: 800 },
|
|
395
|
-
{ text: 'Creating database schema', time: 600 },
|
|
396
|
-
{ text: 'Building API routes', time: 700 },
|
|
397
|
-
{ text: 'Setting up authentication', time: 500 },
|
|
398
|
-
{ text: 'Generating dashboard pages', time: 600 },
|
|
399
|
-
{ text: 'Creating landing page', time: 500 },
|
|
400
|
-
{ text: 'Configuring Docker', time: 400 },
|
|
401
|
-
],
|
|
402
|
-
landing: [
|
|
403
|
-
{ text: 'Generating page structure', time: 500 },
|
|
404
|
-
{ text: 'Writing conversion copy', time: 700 },
|
|
405
|
-
{ text: 'Building hero section', time: 500 },
|
|
406
|
-
{ text: 'Creating feature sections', time: 400 },
|
|
407
|
-
{ text: 'Adding social proof', time: 400 },
|
|
408
|
-
{ text: 'Setting up email capture', time: 300 },
|
|
409
|
-
{ text: 'Adding animations', time: 500 },
|
|
410
|
-
],
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
return [...common, ...(specific[brainId] || specific.mvp)];
|
|
578
|
+
// Return a promise that resolves when REPL ends
|
|
579
|
+
return new Promise((resolve) => {
|
|
580
|
+
rl.on('close', resolve);
|
|
581
|
+
});
|
|
414
582
|
}
|
|
415
583
|
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
│ ├── FAQ.tsx
|
|
478
|
-
│ └── CTA.tsx
|
|
479
|
-
├── package.json
|
|
480
|
-
├── tailwind.config.ts
|
|
481
|
-
└── next.config.js`,
|
|
482
|
-
};
|
|
483
|
-
|
|
484
|
-
return trees[brainId] || trees.mvp;
|
|
584
|
+
// ═══════════════════════════════════════════
|
|
585
|
+
// ERROR HANDLING
|
|
586
|
+
// ═══════════════════════════════════════════
|
|
587
|
+
function handleApiError(err) {
|
|
588
|
+
console.log('');
|
|
589
|
+
|
|
590
|
+
const providerName = getActiveProvider();
|
|
591
|
+
const providerDef = getProviderDef(providerName);
|
|
592
|
+
const providerLabel = providerDef ? providerDef.name : providerName;
|
|
593
|
+
|
|
594
|
+
if (err.message === 'NO_API_KEY') {
|
|
595
|
+
showError(`No API key configured for ${providerLabel}.`);
|
|
596
|
+
showInfo(`Run ${ACCENT('brains config set api_key <your-key>')}`);
|
|
597
|
+
if (providerDef && providerDef.key_env) {
|
|
598
|
+
showInfo(`Or set env var: ${ACCENT(`export ${providerDef.key_env}=<your-key>`)}`);
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (err.status === 401 || err.code === 'invalid_api_key') {
|
|
604
|
+
showError(`Invalid API key for ${providerLabel}.`);
|
|
605
|
+
if (providerDef && providerDef.key_url) {
|
|
606
|
+
showInfo(`Check your key at ${ACCENT(providerDef.key_url)}`);
|
|
607
|
+
}
|
|
608
|
+
showInfo(`Update with: ${ACCENT('brains config set api_key <your-key>')}`);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (err.status === 429) {
|
|
613
|
+
showError('Rate limit exceeded.');
|
|
614
|
+
showInfo('Wait a moment and try again, or check your API usage limits.');
|
|
615
|
+
if (providerDef && providerDef.free) {
|
|
616
|
+
showInfo(`${providerLabel} free tier has rate limits. Consider upgrading or switching providers.`);
|
|
617
|
+
showInfo(`Switch: ${ACCENT('brains config set provider <name>')}`);
|
|
618
|
+
}
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (err.status === 529 || err.status === 503) {
|
|
623
|
+
showError(`${providerLabel} is temporarily overloaded.`);
|
|
624
|
+
showInfo('Try again in a few seconds.');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (err.code === 'ENOTFOUND' || err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
|
|
629
|
+
showError(`Network error — cannot reach ${providerLabel}.`);
|
|
630
|
+
if (providerName === 'ollama') {
|
|
631
|
+
showInfo('Is Ollama running? Start it with: ollama serve');
|
|
632
|
+
} else {
|
|
633
|
+
showInfo('Check your internet connection and try again.');
|
|
634
|
+
}
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Generic error
|
|
639
|
+
showError(`API Error: ${err.message || err}`);
|
|
640
|
+
if (err.status) {
|
|
641
|
+
showInfo(`Status: ${err.status}`);
|
|
642
|
+
}
|
|
643
|
+
showInfo(`Provider: ${providerLabel} | Model: ${getModel()}`);
|
|
644
|
+
console.log('');
|
|
485
645
|
}
|
|
486
646
|
|
|
487
647
|
module.exports = { run };
|