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.
@@ -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 { showSuccess, showError, showInfo, showBox, ACCENT, DIM, SUCCESS, WARN } = require('../utils/ui');
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
- showInfo(` ${WARN('⚠')} ${extraBrain} not found, skipping`);
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
- const categoryColors = {
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
- // ─── Brain-specific workflows ───
131
-
132
- if (brain.category === 'builder') {
133
- await runBuilderBrain(brain, config, options);
134
- } else if (brain.category === 'reviewer') {
135
- await runReviewerBrain(brain, config, options);
136
- } else if (brain.category === 'role') {
137
- await runRoleBrain(brain, config, options);
138
- } else if (brain.category === 'domain') {
139
- await runDomainBrain(brain, config, options);
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
- // ─── Builder Brain Runner ───
144
- async function runBuilderBrain(brain, config, options) {
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('Got it! Generating your project...\n');
163
-
164
- // Simulate generation steps based on brain type
165
- const genSteps = getBuilderSteps(brain.id);
166
- for (const step of genSteps) {
167
- const spinner = ora({
168
- text: step.text,
169
- color: 'magenta',
170
- spinner: 'dots12',
171
- }).start();
172
- await new Promise((r) => setTimeout(r, step.time));
173
- spinner.succeed(chalk.dim(step.text));
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
- // Show output
177
- const dir = options.dir || `./${answers.product || answers.name || brain.id}-project`;
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
- showSuccess('Project generated successfully!\n');
326
+ const writeSpinner = ora({
327
+ text: `Writing ${fileBlocks.length} files to ${projectDir}...`,
328
+ color: 'magenta',
329
+ spinner: 'dots12',
330
+ }).start();
181
331
 
182
- const tree = getProjectTree(brain.id, answers);
183
- console.log(chalk.dim(tree));
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
- showInfo(`Project created at: ${ACCENT(dir)}`);
187
- showInfo(`Next steps:`);
188
- console.log(` ${DIM('$')} ${ACCENT(`cd ${dir}`)}`);
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
- // ─── Reviewer Brain Runner ───
212
- async function runReviewerBrain(brain, config, options) {
213
- const targetDir = options.dir || process.cwd();
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.\n`);
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
- const scanSteps = [
233
- { text: 'Scanning project structure', time: 600 },
234
- { text: 'Analyzing source files', time: 1000 },
235
- { text: 'Checking dependencies', time: 500 },
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
- // Show sample review output
255
- console.log(chalk.bold(' Review Summary\n'));
256
- console.log(` ${chalk.red('🔴 CRITICAL')} ${chalk.dim('0 issues')}`);
257
- console.log(` ${chalk.hex('#FF8800')('🟡 WARNING')} ${chalk.dim('3 issues')}`);
258
- console.log(` ${chalk.hex('#00CC88')('🟢 SUGGESTION')} ${chalk.dim('7 suggestions')}`);
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
- console.log(`\n${chalk.dim('─'.repeat(56))}\n`);
406
+ const systemPrompt = buildSystemPrompt(brain, stackedBrains);
407
+ const focusArea = answers.focus || answers.type || 'general review';
262
408
 
263
- console.log(
264
- ` ${chalk.bold('In production, this brain would:')}\n` +
265
- ` ${chalk.hex('#00AAFF')('▸')} Read every file in your project\n` +
266
- ` ${chalk.hex('#00AAFF')('▸')} Apply the review methodology from its system prompt\n` +
267
- ` ${chalk.hex('#00AAFF')('▸')} Use Claude to analyze patterns, bugs, and security\n` +
268
- ` ${chalk.hex('#00AAFF')('▸')} Output a detailed, prioritized review report\n` +
269
- ` ${chalk.hex('#00AAFF')('▸')} Optionally auto-fix issues with --fix flag\n`
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
- showInfo(`Brain config: ${DIM(`~/.brains/${brain.id}/BRAIN.md`)}`);
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
- // ─── Role Brain Runner ───
277
- async function runRoleBrain(brain, config, options) {
278
- showInfo(`${brain.name} is ready. Starting conversational session.\n`);
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('#7B2FFF')(q.question),
461
+ message: chalk.hex(color)(q.question),
288
462
  },
289
463
  ]);
290
464
  answers[q.key] = answer;
291
465
  }
292
466
 
293
- console.log('');
467
+ const systemPrompt = buildSystemPrompt(brain, stackedBrains);
468
+ const conversationHistory = [];
294
469
 
295
- const spinner = ora({
296
- text: 'Loading context and preparing response...',
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
- console.log(`\n${chalk.dim(''.repeat(56))}\n`);
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
- console.log(
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
- console.log(
315
- ` ${chalk.bold('The system prompt for this brain includes:')}\n` +
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
- // Interactive loop demo
321
- const { continueChat } = await inquirer.prompt([
322
- {
323
- type: 'confirm',
324
- name: 'continueChat',
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
- if (continueChat) {
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
- // ─── Domain Brain Runner ───
338
- async function runDomainBrain(brain, config, options) {
339
- showInfo(`${brain.name} expert session started.\n`);
515
+ rl.on('line', async (line) => {
516
+ const input = line.trim();
340
517
 
341
- // Ask what they need
342
- for (const q of config.questions || brain.questions) {
343
- const { answer } = await inquirer.prompt([
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
- console.log('');
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
- const spinner = ora({
355
- text: `Consulting ${brain.name} knowledge base...`,
356
- color: 'green',
357
- spinner: 'dots12',
358
- }).start();
359
- await new Promise((r) => setTimeout(r, 1200));
360
- spinner.succeed(chalk.dim('Ready'));
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
- console.log(`\n${chalk.dim(''.repeat(56))}\n`);
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
- console.log(
365
- ` ${chalk.bold('In production, this brain would:')}\n` +
366
- ` ${chalk.hex('#00CC88')('▸')} Use deep ${brain.stack[0]} expertise from its system prompt\n` +
367
- ` ${chalk.hex('#00CC88')('▸')} Generate best-practice code specific to your question\n` +
368
- ` ${chalk.hex('#00CC88')('▸')} Include explanations, trade-offs, and gotchas\n` +
369
- ` ${chalk.hex('#00CC88')('▸')} Reference latest patterns and APIs\n` +
370
- ` ${chalk.hex('#00CC88')('▸')} Work interactively in a conversational loop\n`
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
- showInfo(`Brain config: ${DIM(`~/.brains/${brain.id}/BRAIN.md`)}`);
374
- console.log('');
375
- }
571
+ rl.prompt();
572
+ });
573
+
574
+ rl.on('close', () => {
575
+ // Ensure clean exit
576
+ });
376
577
 
377
- // ─── Helper: Builder generation steps ───
378
- function getBuilderSteps(brainId) {
379
- const common = [
380
- { text: 'Analyzing requirements', time: 500 },
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
- // ─── Helper: Project tree output ───
417
- function getProjectTree(brainId, answers) {
418
- const name = answers.product || answers.name || brainId;
419
- const trees = {
420
- resume: `
421
- ${chalk.bold(`${name}-project/`)}
422
- ├── app/
423
- │ ├── layout.tsx
424
- │ ├── page.tsx
425
- │ └── globals.css
426
- ├── components/
427
- │ ├── Hero.tsx
428
- │ ├── Experience.tsx
429
- │ ├── Skills.tsx
430
- │ ├── Projects.tsx
431
- │ └── Contact.tsx
432
- ├── lib/
433
- │ └── data.ts
434
- ├── public/
435
- │ └── resume.pdf
436
- ├── package.json
437
- ├── tailwind.config.ts
438
- ├── tsconfig.json
439
- └── next.config.js`,
440
- mvp: `
441
- ${chalk.bold(`${name}-project/`)}
442
- ├── app/
443
- │ ├── layout.tsx
444
- │ ├── page.tsx ${DIM('← Landing page')}
445
- │ ├── (auth)/
446
- │ │ ├── login/page.tsx
447
- │ │ └── register/page.tsx
448
- │ ├── dashboard/
449
- │ │ ├── layout.tsx
450
- │ │ └── page.tsx
451
- │ └── api/
452
- │ └── [...routes].ts
453
- ├── components/
454
- │ ├── ui/
455
- │ └── features/
456
- ├── lib/
457
- │ ├── db.ts
458
- │ └── auth.ts
459
- ├── prisma/
460
- │ ├── schema.prisma
461
- │ └── migrations/
462
- ├── package.json
463
- ├── docker-compose.yml
464
- └── .env.example`,
465
- landing: `
466
- ${chalk.bold(`${name}-project/`)}
467
- ├── app/
468
- │ ├── layout.tsx
469
- │ ├── page.tsx
470
- │ └── globals.css
471
- ├── components/
472
- │ ├── Hero.tsx
473
- │ ├── Features.tsx
474
- │ ├── HowItWorks.tsx
475
- │ ├── Testimonials.tsx
476
- │ ├── Pricing.tsx
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 };