rampup 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/index.js ADDED
@@ -0,0 +1,2353 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Ramp CLI - Understand any codebase in hours
5
+ * AI-powered developer onboarding
6
+ *
7
+ * Combines:
8
+ * - AI-guided codebase learning
9
+ * - Voice mode for hands-free exploration
10
+ * - Auto-generated onboarding docs
11
+ */
12
+
13
+ import { Command } from 'commander';
14
+ import chalk from 'chalk';
15
+ import ora from 'ora';
16
+ import inquirer from 'inquirer';
17
+ import path from 'path';
18
+ import fs from 'fs/promises';
19
+ import { fileURLToPath } from 'url';
20
+ import { exec } from 'child_process';
21
+ import { promisify } from 'util';
22
+ import omniConfig from './omni/config.js';
23
+ import { checkAndBurnTokens, getTokenBalance } from './entitlements.js';
24
+ import { loginWithBrowser, clearCredentials, getUserInfo, getIdToken } from './auth.js';
25
+ import { saveKnowledge, searchKnowledge, getMyOrg, formatKnowledgeEntry } from './knowledge.js';
26
+
27
+ const execAsync = promisify(exec);
28
+ const __filename = fileURLToPath(import.meta.url);
29
+ const __dirname = path.dirname(__filename);
30
+
31
+ const VERSION = '0.1.0';
32
+
33
+ // ASCII art banner
34
+ const banner = `
35
+ ${chalk.white(' :::- ')}
36
+ ${chalk.white(' :::::-------- ')}
37
+ ${chalk.white('=-:::::::--------:-+')} ${chalk.bold.white('██████╗ █████╗ ███╗ ███╗██████╗')}
38
+ ${chalk.white('++++=-:::::-:....*##')} ${chalk.bold.white('██╔══██╗██╔══██╗████╗ ████║██╔══██╗')}
39
+ ${chalk.white('++++++++=:....:*####')} ${chalk.bold.white('██████╔╝███████║██╔████╔██║██████╔╝')}
40
+ ${chalk.white('*+++++=:....:*######')} ${chalk.bold.white('██╔══██╗██╔══██║██║╚██╔╝██║██╔═══╝')}
41
+ ${chalk.white('**++=:::..:*########')} ${chalk.bold.white('██║ ██║██║ ██║██║ ╚═╝ ██║██║')}
42
+ ${chalk.white('*+=-::::-*##########')} ${chalk.bold.white('╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝')}
43
+ ${chalk.white('+------*############')}
44
+ ${chalk.white(' --*########## ')} ${chalk.gray('Onboard to any codebase in hours')}
45
+ ${chalk.white(' #### ')} ${chalk.cyan('https://rampup.dev')}
46
+ `;
47
+
48
+ const program = new Command();
49
+
50
+ program
51
+ .name('ramp')
52
+ .description('AI-powered developer onboarding - Understand any codebase in hours, not weeks')
53
+ .version(VERSION);
54
+
55
+ // ============================================
56
+ // EXPLORE & LEARN COMMANDS (Onboarding)
57
+ // ============================================
58
+
59
+ // Explore a codebase
60
+ program
61
+ .command('explore [path]')
62
+ .description('Explore and map a codebase structure')
63
+ .option('-d, --depth <number>', 'Max directory depth', '3')
64
+ .option('--no-git', 'Skip git analysis')
65
+ .action(async (targetPath, options) => {
66
+ console.log(banner);
67
+ console.log(chalk.bold.blue('🔍 Exploring Codebase\n'));
68
+
69
+ const projectPath = path.resolve(targetPath || '.');
70
+ const spinner = ora('Scanning project structure...').start();
71
+
72
+ try {
73
+ // Check if directory exists
74
+ await fs.access(projectPath);
75
+
76
+ // Gather project info
77
+ const stats = {
78
+ name: path.basename(projectPath),
79
+ path: projectPath,
80
+ files: { total: 0, byType: {} },
81
+ directories: 0,
82
+ hasPackageJson: false,
83
+ hasGit: false,
84
+ languages: [],
85
+ keyFiles: []
86
+ };
87
+
88
+ // Check for key files
89
+ const keyFileChecks = [
90
+ 'package.json', 'tsconfig.json', 'vite.config.js', 'webpack.config.js',
91
+ 'README.md', '.gitignore', 'Cargo.toml', 'go.mod', 'requirements.txt',
92
+ 'Dockerfile', 'docker-compose.yml', '.env.example'
93
+ ];
94
+
95
+ for (const file of keyFileChecks) {
96
+ try {
97
+ await fs.access(path.join(projectPath, file));
98
+ stats.keyFiles.push(file);
99
+ if (file === 'package.json') stats.hasPackageJson = true;
100
+ } catch {}
101
+ }
102
+
103
+ // Check for git
104
+ try {
105
+ await fs.access(path.join(projectPath, '.git'));
106
+ stats.hasGit = true;
107
+ } catch {}
108
+
109
+ // Scan directory structure
110
+ async function scanDir(dir, depth = 0) {
111
+ if (depth > parseInt(options.depth)) return;
112
+
113
+ const entries = await fs.readdir(dir, { withFileTypes: true });
114
+ for (const entry of entries) {
115
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
116
+
117
+ const fullPath = path.join(dir, entry.name);
118
+ if (entry.isDirectory()) {
119
+ stats.directories++;
120
+ await scanDir(fullPath, depth + 1);
121
+ } else {
122
+ stats.files.total++;
123
+ const ext = path.extname(entry.name).slice(1) || 'other';
124
+ stats.files.byType[ext] = (stats.files.byType[ext] || 0) + 1;
125
+ }
126
+ }
127
+ }
128
+
129
+ await scanDir(projectPath);
130
+
131
+ // Detect languages
132
+ const langMap = {
133
+ js: 'JavaScript', ts: 'TypeScript', tsx: 'TypeScript/React',
134
+ jsx: 'JavaScript/React', py: 'Python', rb: 'Ruby', go: 'Go',
135
+ rs: 'Rust', java: 'Java', cpp: 'C++', c: 'C', swift: 'Swift'
136
+ };
137
+
138
+ for (const [ext, count] of Object.entries(stats.files.byType)) {
139
+ if (langMap[ext] && count > 0) {
140
+ stats.languages.push({ lang: langMap[ext], count });
141
+ }
142
+ }
143
+ stats.languages.sort((a, b) => b.count - a.count);
144
+
145
+ // Read package.json if exists
146
+ let packageInfo = null;
147
+ if (stats.hasPackageJson) {
148
+ try {
149
+ const pkg = JSON.parse(await fs.readFile(path.join(projectPath, 'package.json'), 'utf8'));
150
+ packageInfo = {
151
+ name: pkg.name,
152
+ version: pkg.version,
153
+ description: pkg.description,
154
+ dependencies: Object.keys(pkg.dependencies || {}).length,
155
+ devDependencies: Object.keys(pkg.devDependencies || {}).length,
156
+ scripts: Object.keys(pkg.scripts || {})
157
+ };
158
+ } catch {}
159
+ }
160
+
161
+ spinner.succeed('Scan complete!\n');
162
+
163
+ // Display results
164
+ console.log(chalk.cyan('─── Project Overview ───\n'));
165
+ console.log(chalk.bold(`📁 ${stats.name}`));
166
+ console.log(chalk.dim(` ${stats.path}\n`));
167
+
168
+ console.log(chalk.bold('Stats:'));
169
+ console.log(chalk.dim(` Files: ${stats.files.total}`));
170
+ console.log(chalk.dim(` Directories: ${stats.directories}`));
171
+ console.log(chalk.dim(` Git: ${stats.hasGit ? '✓' : '✗'}\n`));
172
+
173
+ if (stats.languages.length > 0) {
174
+ console.log(chalk.bold('Languages:'));
175
+ stats.languages.slice(0, 5).forEach(({ lang, count }) => {
176
+ console.log(chalk.dim(` ${lang}: ${count} files`));
177
+ });
178
+ console.log('');
179
+ }
180
+
181
+ if (stats.keyFiles.length > 0) {
182
+ console.log(chalk.bold('Key Files:'));
183
+ stats.keyFiles.forEach(f => console.log(chalk.dim(` ${f}`)));
184
+ console.log('');
185
+ }
186
+
187
+ if (packageInfo) {
188
+ console.log(chalk.bold('Package:'));
189
+ console.log(chalk.dim(` ${packageInfo.name}@${packageInfo.version}`));
190
+ if (packageInfo.description) console.log(chalk.dim(` ${packageInfo.description}`));
191
+ console.log(chalk.dim(` ${packageInfo.dependencies} deps, ${packageInfo.devDependencies} devDeps`));
192
+ if (packageInfo.scripts.length > 0) {
193
+ console.log(chalk.dim(` Scripts: ${packageInfo.scripts.slice(0, 5).join(', ')}${packageInfo.scripts.length > 5 ? '...' : ''}`));
194
+ }
195
+ console.log('');
196
+ }
197
+
198
+ console.log(chalk.cyan('────────────────────────\n'));
199
+
200
+ // Offer next steps
201
+ const { nextAction } = await inquirer.prompt([{
202
+ type: 'list',
203
+ name: 'nextAction',
204
+ message: 'What would you like to do?',
205
+ choices: [
206
+ { name: '📚 Learn this codebase (AI-guided tour)', value: 'learn' },
207
+ { name: '🏗️ Get architecture overview', value: 'architect' },
208
+ { name: '🚀 Run a goal on this project', value: 'run' },
209
+ { name: '✅ Done', value: 'done' }
210
+ ]
211
+ }]);
212
+
213
+ if (nextAction === 'learn') {
214
+ await program.parseAsync(['node', 'ramp', 'learn', projectPath]);
215
+ } else if (nextAction === 'architect') {
216
+ const desc = packageInfo?.description || `A ${stats.languages[0]?.lang || 'software'} project`;
217
+ await program.parseAsync(['node', 'ramp', 'design', desc]);
218
+ } else if (nextAction === 'run') {
219
+ console.log(chalk.dim(`\nRun: ramprun "<your goal>" -p ${projectPath}\n`));
220
+ }
221
+
222
+ } catch (error) {
223
+ spinner.fail(`Error: ${error.message}`);
224
+ process.exit(1);
225
+ }
226
+ });
227
+
228
+ // Learn a codebase
229
+ program
230
+ .command('learn [path]')
231
+ .description('AI-guided codebase learning and onboarding')
232
+ .action(async (targetPath) => {
233
+ console.log(banner);
234
+ console.log(chalk.bold.blue('📚 Learn Codebase\n'));
235
+
236
+ const projectPath = path.resolve(targetPath || '.');
237
+ const apiKey = process.env.ANTHROPIC_API_KEY;
238
+
239
+ if (!apiKey) {
240
+ console.error(chalk.red('Error: ANTHROPIC_API_KEY required for AI-guided learning.\n'));
241
+ console.log('Set it with:');
242
+ console.log(chalk.cyan(' export ANTHROPIC_API_KEY=your-api-key\n'));
243
+ process.exit(1);
244
+ }
245
+
246
+ const spinner = ora('Analyzing codebase...').start();
247
+
248
+ try {
249
+ // Gather context
250
+ let context = `Project: ${path.basename(projectPath)}\n`;
251
+
252
+ // Read README if exists
253
+ try {
254
+ const readme = await fs.readFile(path.join(projectPath, 'README.md'), 'utf8');
255
+ context += `\nREADME:\n${readme.slice(0, 2000)}\n`;
256
+ } catch {}
257
+
258
+ // Read package.json if exists
259
+ try {
260
+ const pkg = await fs.readFile(path.join(projectPath, 'package.json'), 'utf8');
261
+ context += `\npackage.json:\n${pkg}\n`;
262
+ } catch {}
263
+
264
+ // Get directory structure
265
+ let structure = '';
266
+ async function getStructure(dir, prefix = '', depth = 0) {
267
+ if (depth > 2) return;
268
+ const entries = await fs.readdir(dir, { withFileTypes: true });
269
+ for (const entry of entries) {
270
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
271
+ structure += `${prefix}${entry.isDirectory() ? '📁' : '📄'} ${entry.name}\n`;
272
+ if (entry.isDirectory()) {
273
+ await getStructure(path.join(dir, entry.name), prefix + ' ', depth + 1);
274
+ }
275
+ }
276
+ }
277
+ await getStructure(projectPath);
278
+ context += `\nStructure:\n${structure.slice(0, 3000)}\n`;
279
+
280
+ spinner.succeed('Codebase analyzed!\n');
281
+
282
+ // Check entitlements before starting
283
+ const idempotencyKey = `learn-${Date.now()}`;
284
+ const entitlementCheck = await checkAndBurnTokens('learn', idempotencyKey);
285
+ if (!entitlementCheck.allowed) {
286
+ console.log(chalk.red(`\n❌ ${entitlementCheck.reason}\n`));
287
+ process.exit(1);
288
+ }
289
+
290
+ // Start interactive learning session
291
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
292
+ const client = new Anthropic({ apiKey });
293
+
294
+ const history = [];
295
+ const systemPrompt = `You are Ramp, an AI coding mentor helping a developer learn a new codebase.
296
+
297
+ Based on this project context:
298
+ ${context}
299
+
300
+ Help the developer understand:
301
+ 1. What this project does
302
+ 2. How it's structured
303
+ 3. Key files and their purposes
304
+ 4. How to get started contributing
305
+
306
+ Be concise, friendly, and practical. Use the project's actual files and structure in your explanations.
307
+ Start by giving a brief overview of the project.`;
308
+
309
+ // Initial overview
310
+ const overviewSpinner = ora('Generating overview...').start();
311
+ const overview = await client.messages.create({
312
+ model: 'claude-sonnet-4-20250514',
313
+ max_tokens: 1500,
314
+ system: systemPrompt,
315
+ messages: [{ role: 'user', content: 'Give me a quick overview of this project.' }]
316
+ });
317
+
318
+ overviewSpinner.stop();
319
+ const overviewText = overview.content[0].type === 'text' ? overview.content[0].text : '';
320
+ history.push({ role: 'user', content: 'Give me a quick overview of this project.' });
321
+ history.push({ role: 'assistant', content: overviewText });
322
+
323
+ console.log(chalk.cyan('─── Project Overview ───\n'));
324
+ console.log(overviewText);
325
+ console.log(chalk.cyan('\n────────────────────────\n'));
326
+
327
+ console.log(chalk.gray('Ask questions about the codebase. Type /exit to quit.\n'));
328
+
329
+ // Interactive Q&A
330
+ while (true) {
331
+ const { question } = await inquirer.prompt([{
332
+ type: 'input',
333
+ name: 'question',
334
+ message: chalk.green('You:'),
335
+ prefix: ''
336
+ }]);
337
+
338
+ if (!question.trim()) continue;
339
+ if (question.toLowerCase() === '/exit' || question.toLowerCase() === '/quit') {
340
+ console.log(chalk.cyan('\nHappy coding! 🚀\n'));
341
+ break;
342
+ }
343
+
344
+ history.push({ role: 'user', content: question });
345
+
346
+ const thinkingSpinner = ora('Thinking...').start();
347
+ const response = await client.messages.create({
348
+ model: 'claude-sonnet-4-20250514',
349
+ max_tokens: 1500,
350
+ system: systemPrompt,
351
+ messages: history
352
+ });
353
+ thinkingSpinner.stop();
354
+
355
+ const answer = response.content[0].type === 'text' ? response.content[0].text : '';
356
+ history.push({ role: 'assistant', content: answer });
357
+
358
+ console.log(chalk.cyan('\n─── Ramp ───\n'));
359
+ console.log(answer);
360
+ console.log(chalk.cyan('\n─────────────\n'));
361
+ }
362
+
363
+ } catch (error) {
364
+ spinner.fail(`Error: ${error.message}`);
365
+ process.exit(1);
366
+ }
367
+ });
368
+
369
+ // Start - First time onboarding
370
+ program
371
+ .command('start')
372
+ .alias('init')
373
+ .description('Start onboarding to a new codebase')
374
+ .action(async () => {
375
+ console.log(banner);
376
+ console.log(chalk.bold.blue('🚀 Welcome to Ramp!\n'));
377
+ console.log(chalk.gray('Let\'s get you up to speed on this codebase.\n'));
378
+
379
+ const projectPath = process.cwd();
380
+
381
+ // Check if we're in a code project
382
+ let hasCode = false;
383
+ const codeIndicators = ['package.json', 'Cargo.toml', 'go.mod', 'requirements.txt', 'pom.xml', '.git'];
384
+ for (const file of codeIndicators) {
385
+ try {
386
+ await fs.access(path.join(projectPath, file));
387
+ hasCode = true;
388
+ break;
389
+ } catch {}
390
+ }
391
+
392
+ if (!hasCode) {
393
+ console.log(chalk.yellow('No codebase detected in current directory.\n'));
394
+ const { navTo } = await inquirer.prompt([{
395
+ type: 'input',
396
+ name: 'navTo',
397
+ message: 'Enter path to codebase (or press enter to continue here):',
398
+ default: '.'
399
+ }]);
400
+ if (navTo !== '.') {
401
+ console.log(chalk.dim(`\nRun: cd ${navTo} && rampstart\n`));
402
+ return;
403
+ }
404
+ }
405
+
406
+ console.log(chalk.cyan('─────────────────────────────────────────\n'));
407
+
408
+ // Step 1: Quick scan
409
+ console.log(chalk.bold('Step 1: Quick Scan\n'));
410
+ await program.parseAsync(['node', 'ramp', 'explore', projectPath, '-d', '2']);
411
+
412
+ // Step 2: Offer learning
413
+ console.log(chalk.bold('\nStep 2: Guided Tour\n'));
414
+ const { wantTour } = await inquirer.prompt([{
415
+ type: 'confirm',
416
+ name: 'wantTour',
417
+ message: 'Want an AI-guided tour of this codebase?',
418
+ default: true
419
+ }]);
420
+
421
+ if (wantTour) {
422
+ await program.parseAsync(['node', 'ramp', 'learn', projectPath]);
423
+ } else {
424
+ console.log(chalk.dim('\nYou can start a tour anytime with: ramplearn\n'));
425
+ console.log(chalk.bold('Quick commands:'));
426
+ console.log(chalk.dim(' rampask "how does auth work?" - Ask about the code'));
427
+ console.log(chalk.dim(' rampexplore - See project structure'));
428
+ console.log(chalk.dim(' rampguide - Generate onboarding doc\n'));
429
+ }
430
+ });
431
+
432
+ // Ask - Quick questions about the codebase
433
+ program
434
+ .command('ask <question>')
435
+ .description('Ask a question about the codebase')
436
+ .option('-p, --path <path>', 'Project path', '.')
437
+ .action(async (question, options) => {
438
+ const projectPath = path.resolve(options.path);
439
+ const apiKey = process.env.ANTHROPIC_API_KEY;
440
+
441
+ if (!apiKey) {
442
+ console.error(chalk.red('Error: ANTHROPIC_API_KEY required.\n'));
443
+ console.log(chalk.cyan(' export ANTHROPIC_API_KEY=your-api-key\n'));
444
+ process.exit(1);
445
+ }
446
+
447
+ const spinner = ora('Reading codebase...').start();
448
+
449
+ try {
450
+ // Gather context
451
+ let context = `Project: ${path.basename(projectPath)}\n`;
452
+
453
+ // Read key files
454
+ const keyFiles = ['README.md', 'package.json', 'tsconfig.json', 'Cargo.toml', 'go.mod'];
455
+ for (const file of keyFiles) {
456
+ try {
457
+ const content = await fs.readFile(path.join(projectPath, file), 'utf8');
458
+ context += `\n${file}:\n${content.slice(0, 1500)}\n`;
459
+ } catch {}
460
+ }
461
+
462
+ // Get structure
463
+ let structure = '';
464
+ async function getStructure(dir, prefix = '', depth = 0) {
465
+ if (depth > 2) return;
466
+ try {
467
+ const entries = await fs.readdir(dir, { withFileTypes: true });
468
+ for (const entry of entries) {
469
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
470
+ structure += `${prefix}${entry.name}${entry.isDirectory() ? '/' : ''}\n`;
471
+ if (entry.isDirectory() && depth < 2) {
472
+ await getStructure(path.join(dir, entry.name), prefix + ' ', depth + 1);
473
+ }
474
+ }
475
+ } catch {}
476
+ }
477
+ await getStructure(projectPath);
478
+ context += `\nStructure:\n${structure.slice(0, 2000)}\n`;
479
+
480
+ // Check entitlements
481
+ const idempotencyKey = `ask-${Date.now()}`;
482
+ const entitlementCheck = await checkAndBurnTokens('ask', idempotencyKey);
483
+ if (!entitlementCheck.allowed) {
484
+ spinner.fail(entitlementCheck.reason);
485
+ process.exit(1);
486
+ }
487
+
488
+ spinner.text = 'Thinking...';
489
+
490
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
491
+ const client = new Anthropic({ apiKey });
492
+
493
+ const response = await client.messages.create({
494
+ model: 'claude-sonnet-4-20250514',
495
+ max_tokens: 1500,
496
+ system: `You are Ramp, helping a developer understand a codebase.
497
+
498
+ Project context:
499
+ ${context}
500
+
501
+ Answer questions concisely and practically. Reference specific files when relevant.`,
502
+ messages: [{ role: 'user', content: question }]
503
+ });
504
+
505
+ spinner.stop();
506
+
507
+ const answer = response.content[0].type === 'text' ? response.content[0].text : '';
508
+ console.log(chalk.cyan('\n─── Answer ───\n'));
509
+ console.log(answer);
510
+ console.log(chalk.cyan('\n──────────────\n'));
511
+
512
+ } catch (error) {
513
+ spinner.fail(`Error: ${error.message}`);
514
+ }
515
+ });
516
+
517
+ // Progress - Track onboarding progress
518
+ program
519
+ .command('progress')
520
+ .description('View your onboarding progress')
521
+ .action(async () => {
522
+ console.log(banner);
523
+ console.log(chalk.bold.blue('📊 Your Progress\n'));
524
+
525
+ const projectPath = process.cwd();
526
+ const projectName = path.basename(projectPath);
527
+ const progressFile = path.join(process.env.HOME, '.ramp', 'progress.json');
528
+
529
+ let progress = {};
530
+ try {
531
+ progress = JSON.parse(await fs.readFile(progressFile, 'utf8'));
532
+ } catch {}
533
+
534
+ const projectProgress = progress[projectPath] || {
535
+ started: null,
536
+ explored: false,
537
+ learned: false,
538
+ questionsAsked: 0,
539
+ filesExplored: [],
540
+ lastActive: null
541
+ };
542
+
543
+ if (!projectProgress.started) {
544
+ console.log(chalk.yellow(`You haven't started onboarding to ${projectName} yet.\n`));
545
+ console.log(chalk.dim('Run: rampstart\n'));
546
+ return;
547
+ }
548
+
549
+ console.log(chalk.bold(`Project: ${projectName}\n`));
550
+ console.log(chalk.dim(`Started: ${new Date(projectProgress.started).toLocaleDateString()}`));
551
+ console.log(chalk.dim(`Last active: ${projectProgress.lastActive ? new Date(projectProgress.lastActive).toLocaleDateString() : 'Never'}\n`));
552
+
553
+ console.log(chalk.bold('Milestones:'));
554
+ console.log(` ${projectProgress.explored ? '✅' : '⬜'} Explored codebase structure`);
555
+ console.log(` ${projectProgress.learned ? '✅' : '⬜'} Completed AI-guided tour`);
556
+ console.log(` ${projectProgress.questionsAsked >= 5 ? '✅' : '⬜'} Asked 5+ questions (${projectProgress.questionsAsked}/5)`);
557
+ console.log(` ${projectProgress.filesExplored.length >= 10 ? '✅' : '⬜'} Explored 10+ files (${projectProgress.filesExplored.length}/10)\n`);
558
+
559
+ const completed = [
560
+ projectProgress.explored,
561
+ projectProgress.learned,
562
+ projectProgress.questionsAsked >= 5,
563
+ projectProgress.filesExplored.length >= 10
564
+ ].filter(Boolean).length;
565
+
566
+ const percentage = Math.round((completed / 4) * 100);
567
+ const bar = '█'.repeat(Math.floor(percentage / 10)) + '░'.repeat(10 - Math.floor(percentage / 10));
568
+ console.log(chalk.bold(`Progress: [${bar}] ${percentage}%\n`));
569
+
570
+ if (percentage < 100) {
571
+ console.log(chalk.dim('Next steps:'));
572
+ if (!projectProgress.explored) console.log(chalk.dim(' → rampexplore'));
573
+ if (!projectProgress.learned) console.log(chalk.dim(' → ramplearn'));
574
+ if (projectProgress.questionsAsked < 5) console.log(chalk.dim(' → rampask "your question"'));
575
+ console.log('');
576
+ } else {
577
+ console.log(chalk.green('🎉 You\'re fully onboarded! Nice work.\n'));
578
+ }
579
+ });
580
+
581
+ // Guide - Generate onboarding documentation
582
+ program
583
+ .command('guide')
584
+ .description('Generate an onboarding guide for this codebase')
585
+ .option('-o, --output <file>', 'Output file', 'ONBOARDING.md')
586
+ .action(async (options) => {
587
+ console.log(banner);
588
+ console.log(chalk.bold.blue('📖 Generating Onboarding Guide\n'));
589
+
590
+ const projectPath = process.cwd();
591
+ const apiKey = process.env.ANTHROPIC_API_KEY;
592
+
593
+ if (!apiKey) {
594
+ console.error(chalk.red('Error: ANTHROPIC_API_KEY required.\n'));
595
+ process.exit(1);
596
+ }
597
+
598
+ const spinner = ora('Analyzing codebase...').start();
599
+
600
+ try {
601
+ // Gather comprehensive context
602
+ let context = `Project: ${path.basename(projectPath)}\n`;
603
+
604
+ // Read README
605
+ try {
606
+ const readme = await fs.readFile(path.join(projectPath, 'README.md'), 'utf8');
607
+ context += `\nExisting README:\n${readme.slice(0, 3000)}\n`;
608
+ } catch {}
609
+
610
+ // Read package.json or equivalent
611
+ try {
612
+ const pkg = await fs.readFile(path.join(projectPath, 'package.json'), 'utf8');
613
+ context += `\npackage.json:\n${pkg}\n`;
614
+ } catch {}
615
+
616
+ // Get full structure
617
+ let structure = '';
618
+ async function getStructure(dir, prefix = '', depth = 0) {
619
+ if (depth > 3) return;
620
+ try {
621
+ const entries = await fs.readdir(dir, { withFileTypes: true });
622
+ for (const entry of entries) {
623
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist') continue;
624
+ structure += `${prefix}${entry.name}${entry.isDirectory() ? '/' : ''}\n`;
625
+ if (entry.isDirectory()) {
626
+ await getStructure(path.join(dir, entry.name), prefix + ' ', depth + 1);
627
+ }
628
+ }
629
+ } catch {}
630
+ }
631
+ await getStructure(projectPath);
632
+ context += `\nFull structure:\n${structure.slice(0, 5000)}\n`;
633
+
634
+ // Sample some key files
635
+ const sampleFiles = ['src/index.ts', 'src/index.js', 'src/main.ts', 'src/App.tsx', 'src/app.py', 'main.go'];
636
+ for (const file of sampleFiles) {
637
+ try {
638
+ const content = await fs.readFile(path.join(projectPath, file), 'utf8');
639
+ context += `\n${file}:\n${content.slice(0, 1000)}\n`;
640
+ } catch {}
641
+ }
642
+
643
+ // Check entitlements
644
+ const idempotencyKey = `guide-${Date.now()}`;
645
+ const entitlementCheck = await checkAndBurnTokens('guide', idempotencyKey);
646
+ if (!entitlementCheck.allowed) {
647
+ spinner.fail(entitlementCheck.reason);
648
+ process.exit(1);
649
+ }
650
+
651
+ spinner.text = 'Generating guide...';
652
+
653
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
654
+ const client = new Anthropic({ apiKey });
655
+
656
+ const response = await client.messages.create({
657
+ model: 'claude-sonnet-4-20250514',
658
+ max_tokens: 4000,
659
+ system: `You are creating an onboarding guide for new developers joining this project.
660
+
661
+ Project context:
662
+ ${context}
663
+
664
+ Generate a comprehensive ONBOARDING.md that includes:
665
+ 1. Project Overview (what it does, who it's for)
666
+ 2. Architecture Overview (high-level structure, key patterns)
667
+ 3. Getting Started (setup steps, prerequisites)
668
+ 4. Key Concepts (important abstractions, terminology)
669
+ 5. Directory Guide (what each major folder contains)
670
+ 6. Common Tasks (how to add features, fix bugs, run tests)
671
+ 7. Key Files to Read First (most important files for understanding)
672
+ 8. Gotchas & Tips (things that might trip up new devs)
673
+
674
+ Be specific to THIS codebase. Use actual file names and paths.
675
+ Format as clean Markdown.`,
676
+ messages: [{ role: 'user', content: 'Generate the onboarding guide.' }]
677
+ });
678
+
679
+ spinner.stop();
680
+
681
+ const guide = response.content[0].type === 'text' ? response.content[0].text : '';
682
+
683
+ // Save to file
684
+ await fs.writeFile(path.join(projectPath, options.output), guide);
685
+ console.log(chalk.green(`✓ Generated ${options.output}\n`));
686
+
687
+ // Preview
688
+ console.log(chalk.cyan('─── Preview ───\n'));
689
+ console.log(guide.slice(0, 1500) + (guide.length > 1500 ? '\n\n...(truncated)' : ''));
690
+ console.log(chalk.cyan('\n───────────────\n'));
691
+
692
+ console.log(chalk.dim(`Full guide saved to: ${options.output}\n`));
693
+
694
+ } catch (error) {
695
+ spinner.fail(`Error: ${error.message}`);
696
+ }
697
+ });
698
+
699
+ // ============================================
700
+ // VOICE COMMAND - Talk to your codebase
701
+ // ============================================
702
+
703
+ program
704
+ .command('voice')
705
+ .description('Voice-based codebase learning (talk to your code)')
706
+ .option('-p, --path <path>', 'Project path', '.')
707
+ .action(async (options) => {
708
+ console.log(chalk.bold.blue('\n🎙️ Voice Mode\n'));
709
+ console.log(chalk.gray('Talk to your codebase. Say "exit" or press Ctrl+C to quit.\n'));
710
+
711
+ const projectPath = path.resolve(options.path);
712
+
713
+ // Check if user is logged in
714
+ const token = await getIdToken();
715
+ if (!token) {
716
+ console.log(chalk.yellow('Please log in to use voice mode.\n'));
717
+ const { shouldLogin } = await inquirer.prompt([{
718
+ type: 'confirm',
719
+ name: 'shouldLogin',
720
+ message: 'Would you like to log in now?',
721
+ default: true,
722
+ }]);
723
+
724
+ if (shouldLogin) {
725
+ try {
726
+ await loginWithBrowser();
727
+ console.log(chalk.green('\n✓ Logged in successfully!\n'));
728
+ } catch (error) {
729
+ console.error(chalk.red(`Login failed: ${error.message}`));
730
+ process.exit(1);
731
+ }
732
+ } else {
733
+ console.log(chalk.dim('\nRun `ramp login` to authenticate.\n'));
734
+ process.exit(0);
735
+ }
736
+ }
737
+
738
+ // Get fresh token after potential login
739
+ const authToken = await getIdToken();
740
+
741
+ const API_URL = process.env.RAMP_API_URL || 'https://entitlement-service.rian-19c.workers.dev';
742
+
743
+ // Track usage
744
+ const usageFile = path.join(process.env.HOME, '.ramp', 'voice-usage.json');
745
+ let usage = { totalMinutes: 0, sessions: [] };
746
+ try {
747
+ await fs.mkdir(path.join(process.env.HOME, '.ramp'), { recursive: true });
748
+ usage = JSON.parse(await fs.readFile(usageFile, 'utf8'));
749
+ } catch {}
750
+
751
+ const sessionStart = Date.now();
752
+ let sessionMinutes = 0;
753
+
754
+ // Gather codebase context once
755
+ const spinner = ora('Reading codebase...').start();
756
+ let context = `Project: ${path.basename(projectPath)}\n`;
757
+
758
+ try {
759
+ const keyFiles = ['README.md', 'package.json', 'tsconfig.json'];
760
+ for (const file of keyFiles) {
761
+ try {
762
+ const content = await fs.readFile(path.join(projectPath, file), 'utf8');
763
+ context += `\n${file}:\n${content.slice(0, 1500)}\n`;
764
+ } catch {}
765
+ }
766
+
767
+ let structure = '';
768
+ async function getStructure(dir, prefix = '', depth = 0) {
769
+ if (depth > 2) return;
770
+ try {
771
+ const entries = await fs.readdir(dir, { withFileTypes: true });
772
+ for (const entry of entries) {
773
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
774
+ structure += `${prefix}${entry.name}${entry.isDirectory() ? '/' : ''}\n`;
775
+ if (entry.isDirectory()) {
776
+ await getStructure(path.join(dir, entry.name), prefix + ' ', depth + 1);
777
+ }
778
+ }
779
+ } catch {}
780
+ }
781
+ await getStructure(projectPath);
782
+ context += `\nStructure:\n${structure.slice(0, 2000)}\n`;
783
+
784
+ spinner.succeed('Ready! Listening...\n');
785
+ } catch (error) {
786
+ spinner.fail(`Error: ${error.message}`);
787
+ process.exit(1);
788
+ }
789
+
790
+ const conversationHistory = [];
791
+
792
+ // Helper function to call backend chat API
793
+ async function chatWithBackend(messages, systemPrompt) {
794
+ const response = await fetch(`${API_URL}/ai/chat`, {
795
+ method: 'POST',
796
+ headers: {
797
+ 'Authorization': `Bearer ${authToken}`,
798
+ 'Content-Type': 'application/json',
799
+ },
800
+ body: JSON.stringify({
801
+ product: 'ramp',
802
+ messages,
803
+ system: systemPrompt,
804
+ max_tokens: 500,
805
+ }),
806
+ });
807
+
808
+ if (!response.ok) {
809
+ const error = await response.json().catch(() => ({}));
810
+ throw new Error(error.message || `API error: ${response.status}`);
811
+ }
812
+
813
+ return await response.json();
814
+ }
815
+
816
+ // Helper function to call backend TTS API
817
+ async function textToSpeech(text) {
818
+ const response = await fetch(`${API_URL}/ai/tts`, {
819
+ method: 'POST',
820
+ headers: {
821
+ 'Authorization': `Bearer ${authToken}`,
822
+ 'Content-Type': 'application/json',
823
+ },
824
+ body: JSON.stringify({
825
+ product: 'ramp',
826
+ text,
827
+ voice: 'nova',
828
+ }),
829
+ });
830
+
831
+ if (!response.ok) {
832
+ throw new Error(`TTS error: ${response.status}`);
833
+ }
834
+
835
+ return Buffer.from(await response.arrayBuffer());
836
+ }
837
+
838
+ // Voice interaction loop
839
+ async function voiceLoop() {
840
+ while (true) {
841
+ try {
842
+ // For now, use text input with voice output
843
+ // Full voice input requires native audio recording
844
+ const { input } = await inquirer.prompt([{
845
+ type: 'input',
846
+ name: 'input',
847
+ message: chalk.green('🎤 You:'),
848
+ prefix: ''
849
+ }]);
850
+
851
+ if (!input.trim()) continue;
852
+ if (input.toLowerCase() === 'exit' || input.toLowerCase() === 'quit') {
853
+ break;
854
+ }
855
+
856
+ const startTime = Date.now();
857
+ conversationHistory.push({ role: 'user', content: input });
858
+
859
+ // Get AI response
860
+ const thinkingSpinner = ora('Thinking...').start();
861
+
862
+ const systemPrompt = `You are Ramp, a voice assistant helping a developer understand a codebase.
863
+ Keep responses concise (2-3 sentences) since they'll be spoken aloud.
864
+
865
+ Project context:
866
+ ${context}
867
+
868
+ Be helpful, friendly, and practical. Reference specific files when relevant.`;
869
+
870
+ const chatResponse = await chatWithBackend(conversationHistory, systemPrompt);
871
+ const answer = chatResponse.content || chatResponse.text || '';
872
+ conversationHistory.push({ role: 'assistant', content: answer });
873
+
874
+ thinkingSpinner.stop();
875
+
876
+ // Generate speech
877
+ const speechSpinner = ora('Speaking...').start();
878
+
879
+ try {
880
+ const audioBuffer = await textToSpeech(answer);
881
+
882
+ // Save and play audio
883
+ const audioPath = `/tmp/ramp-voice-${Date.now()}.mp3`;
884
+ await fs.writeFile(audioPath, audioBuffer);
885
+
886
+ speechSpinner.stop();
887
+ console.log(chalk.cyan(`\n🔊 Ramp: ${answer}\n`));
888
+
889
+ // Play audio (macOS)
890
+ if (process.platform === 'darwin') {
891
+ await execAsync(`afplay "${audioPath}"`).catch(() => {});
892
+ } else if (process.platform === 'linux') {
893
+ await execAsync(`mpg123 "${audioPath}" 2>/dev/null || play "${audioPath}" 2>/dev/null`).catch(() => {});
894
+ }
895
+
896
+ // Clean up
897
+ await fs.unlink(audioPath).catch(() => {});
898
+
899
+ } catch (ttsError) {
900
+ speechSpinner.stop();
901
+ // Fallback to text if TTS fails
902
+ console.log(chalk.cyan(`\n💬 Ramp: ${answer}\n`));
903
+ }
904
+
905
+ // Track usage
906
+ const elapsed = (Date.now() - startTime) / 1000 / 60;
907
+ sessionMinutes += elapsed;
908
+
909
+ } catch (error) {
910
+ if (error.name === 'ExitPromptError') break;
911
+ console.error(chalk.red(`Error: ${error.message}`));
912
+ }
913
+ }
914
+ }
915
+
916
+ // Handle exit
917
+ process.on('SIGINT', async () => {
918
+ console.log(chalk.cyan('\n\n👋 Ending voice session...\n'));
919
+ await saveUsage();
920
+ process.exit(0);
921
+ });
922
+
923
+ async function saveUsage() {
924
+ const totalSessionMinutes = (Date.now() - sessionStart) / 1000 / 60;
925
+ usage.totalMinutes += totalSessionMinutes;
926
+ usage.sessions.push({
927
+ date: new Date().toISOString(),
928
+ project: path.basename(projectPath),
929
+ minutes: totalSessionMinutes
930
+ });
931
+ await fs.writeFile(usageFile, JSON.stringify(usage, null, 2));
932
+
933
+ console.log(chalk.dim(`Session: ${totalSessionMinutes.toFixed(2)} min`));
934
+ console.log(chalk.dim(`Total usage: ${usage.totalMinutes.toFixed(2)} min\n`));
935
+ }
936
+
937
+ await voiceLoop();
938
+ await saveUsage();
939
+ });
940
+
941
+ // Voice usage stats
942
+ program
943
+ .command('voice:usage')
944
+ .description('View voice minutes usage')
945
+ .action(async () => {
946
+ console.log(banner);
947
+ console.log(chalk.bold.blue('📊 Voice Usage\n'));
948
+
949
+ const usageFile = path.join(process.env.HOME, '.ramp', 'voice-usage.json');
950
+ let usage = { totalMinutes: 0, sessions: [] };
951
+
952
+ try {
953
+ usage = JSON.parse(await fs.readFile(usageFile, 'utf8'));
954
+ } catch {
955
+ console.log(chalk.dim('No voice usage yet.\n'));
956
+ console.log(chalk.dim('Start with: ramp voice\n'));
957
+ return;
958
+ }
959
+
960
+ console.log(chalk.bold(`Total: ${usage.totalMinutes.toFixed(2)} minutes\n`));
961
+
962
+ if (usage.sessions.length > 0) {
963
+ console.log(chalk.bold('Recent Sessions:'));
964
+ usage.sessions.slice(-10).reverse().forEach(s => {
965
+ const date = new Date(s.date).toLocaleDateString();
966
+ console.log(chalk.dim(` ${date} - ${s.project}: ${s.minutes.toFixed(2)} min`));
967
+ });
968
+ console.log('');
969
+ }
970
+
971
+ // Show tier info
972
+ const freeMinutes = 10;
973
+ const remaining = Math.max(0, freeMinutes - usage.totalMinutes);
974
+
975
+ console.log(chalk.bold('Plan: Free Tier'));
976
+ console.log(chalk.dim(` ${remaining.toFixed(2)} / ${freeMinutes} minutes remaining\n`));
977
+
978
+ if (remaining <= 0) {
979
+ console.log(chalk.yellow('⚡ Upgrade to Pro for unlimited voice minutes'));
980
+ console.log(chalk.dim(' rampupgrade\n'));
981
+ }
982
+ });
983
+
984
+ // ============================================
985
+ // BILLING & ACCOUNT
986
+ // ============================================
987
+
988
+ program
989
+ .command('login')
990
+ .description('Log in to your Ramp account')
991
+ .action(async () => {
992
+ console.log(banner);
993
+ console.log(chalk.bold.blue('🔐 Login\n'));
994
+
995
+ const configDir = path.join(process.env.HOME, '.ramp');
996
+ const authFile = path.join(configDir, 'auth.json');
997
+
998
+ // Check if already logged in
999
+ try {
1000
+ const auth = JSON.parse(await fs.readFile(authFile, 'utf8'));
1001
+ if (auth.token) {
1002
+ console.log(chalk.green(`Already logged in as ${auth.email}\n`));
1003
+ const { logout } = await inquirer.prompt([{
1004
+ type: 'confirm',
1005
+ name: 'logout',
1006
+ message: 'Log out?',
1007
+ default: false
1008
+ }]);
1009
+ if (logout) {
1010
+ await fs.unlink(authFile);
1011
+ console.log(chalk.dim('\nLogged out.\n'));
1012
+ }
1013
+ return;
1014
+ }
1015
+ } catch {}
1016
+
1017
+ console.log(chalk.dim('Create an account at: https://rampup.dev\n'));
1018
+
1019
+ const { email, token } = await inquirer.prompt([
1020
+ {
1021
+ type: 'input',
1022
+ name: 'email',
1023
+ message: 'Email:'
1024
+ },
1025
+ {
1026
+ type: 'password',
1027
+ name: 'token',
1028
+ message: 'API Token (from rampup.dev/settings):'
1029
+ }
1030
+ ]);
1031
+
1032
+ // In production, validate token against backend
1033
+ // For now, just save it
1034
+ await fs.mkdir(configDir, { recursive: true });
1035
+ await fs.writeFile(authFile, JSON.stringify({ email, token, created: new Date().toISOString() }, null, 2));
1036
+
1037
+ console.log(chalk.green('\n✓ Logged in successfully!\n'));
1038
+ });
1039
+
1040
+ program
1041
+ .command('upgrade')
1042
+ .description('Upgrade your Ramp plan')
1043
+ .action(async () => {
1044
+ console.log(banner);
1045
+ console.log(chalk.bold.blue('⚡ Upgrade Plan\n'));
1046
+
1047
+ // Check current usage
1048
+ const usageFile = path.join(process.env.HOME, '.ramp', 'voice-usage.json');
1049
+ let usage = { totalMinutes: 0 };
1050
+ try {
1051
+ usage = JSON.parse(await fs.readFile(usageFile, 'utf8'));
1052
+ } catch {}
1053
+
1054
+ console.log(chalk.bold('Current: Free Tier'));
1055
+ console.log(chalk.dim(` Voice: ${usage.totalMinutes.toFixed(2)} / 10 min used\n`));
1056
+
1057
+ console.log(chalk.bold('Available Plans:\n'));
1058
+
1059
+ console.log(chalk.cyan(' Starter - $29/mo'));
1060
+ console.log(chalk.dim(' • 100 voice minutes'));
1061
+ console.log(chalk.dim(' • Unlimited text queries'));
1062
+ console.log(chalk.dim(' • 5 team members\n'));
1063
+
1064
+ console.log(chalk.cyan(' Pro - $99/mo'));
1065
+ console.log(chalk.dim(' • 500 voice minutes'));
1066
+ console.log(chalk.dim(' • Unlimited everything'));
1067
+ console.log(chalk.dim(' • 25 team members'));
1068
+ console.log(chalk.dim(' • Priority support\n'));
1069
+
1070
+ console.log(chalk.cyan(' Enterprise - Custom'));
1071
+ console.log(chalk.dim(' • Unlimited voice minutes'));
1072
+ console.log(chalk.dim(' • Unlimited team members'));
1073
+ console.log(chalk.dim(' • SSO, audit logs'));
1074
+ console.log(chalk.dim(' • Dedicated support\n'));
1075
+
1076
+ console.log(chalk.dim('Upgrade at: https://rampup.dev/pricing\n'));
1077
+
1078
+ const { openBrowser } = await inquirer.prompt([{
1079
+ type: 'confirm',
1080
+ name: 'openBrowser',
1081
+ message: 'Open pricing page?',
1082
+ default: true
1083
+ }]);
1084
+
1085
+ if (openBrowser) {
1086
+ const open = (await import('open')).default;
1087
+ await open('https://rampup.dev/pricing');
1088
+ }
1089
+ });
1090
+
1091
+ program
1092
+ .command('status')
1093
+ .description('View account status and usage')
1094
+ .action(async () => {
1095
+ console.log(banner);
1096
+ console.log(chalk.bold.blue('📊 Account Status\n'));
1097
+
1098
+ const configDir = path.join(process.env.HOME, '.ramp');
1099
+
1100
+ // Auth status
1101
+ let auth = null;
1102
+ try {
1103
+ auth = JSON.parse(await fs.readFile(path.join(configDir, 'auth.json'), 'utf8'));
1104
+ } catch {}
1105
+
1106
+ if (auth) {
1107
+ console.log(chalk.bold('Account:'));
1108
+ console.log(chalk.dim(` Email: ${auth.email}`));
1109
+ console.log(chalk.dim(` Plan: Free Tier\n`));
1110
+ } else {
1111
+ console.log(chalk.yellow('Not logged in.'));
1112
+ console.log(chalk.dim(' Run: ramplogin\n'));
1113
+ }
1114
+
1115
+ // Voice usage
1116
+ let voiceUsage = { totalMinutes: 0 };
1117
+ try {
1118
+ voiceUsage = JSON.parse(await fs.readFile(path.join(configDir, 'voice-usage.json'), 'utf8'));
1119
+ } catch {}
1120
+
1121
+ console.log(chalk.bold('Usage This Month:'));
1122
+ const freeMinutes = 10;
1123
+ const voiceRemaining = Math.max(0, freeMinutes - voiceUsage.totalMinutes);
1124
+ const voicePercent = Math.min(100, (voiceUsage.totalMinutes / freeMinutes) * 100);
1125
+ const voiceBar = '█'.repeat(Math.floor(voicePercent / 10)) + '░'.repeat(10 - Math.floor(voicePercent / 10));
1126
+ console.log(chalk.dim(` Voice: [${voiceBar}] ${voiceUsage.totalMinutes.toFixed(1)}/${freeMinutes} min`));
1127
+
1128
+ // Progress data
1129
+ let progress = {};
1130
+ try {
1131
+ progress = JSON.parse(await fs.readFile(path.join(configDir, 'progress.json'), 'utf8'));
1132
+ } catch {}
1133
+ const projectCount = Object.keys(progress).length;
1134
+ console.log(chalk.dim(` Projects explored: ${projectCount}`));
1135
+
1136
+ console.log('');
1137
+
1138
+ if (voiceRemaining <= 2) {
1139
+ console.log(chalk.yellow('⚠️ Running low on voice minutes!'));
1140
+ console.log(chalk.dim(' Run: rampupgrade\n'));
1141
+ }
1142
+ });
1143
+
1144
+ // ============================================
1145
+ // ORCHESTRATION COMMANDS (Secondary)
1146
+ // ============================================
1147
+
1148
+ // Run a goal
1149
+ program
1150
+ .command('run <goal>')
1151
+ .description('Execute a development goal with AI orchestration')
1152
+ .option('-p, --project <path>', 'Project path', process.cwd())
1153
+ .option('--task-manager <provider>', 'Task manager AI provider', 'anthropic')
1154
+ .option('--task-manager-model <model>', 'Task manager model', 'claude-sonnet-4-20250514')
1155
+ .option('--operator <provider>', 'Operator AI provider', 'anthropic')
1156
+ .option('--operator-model <model>', 'Operator model', 'claude-sonnet-4-20250514')
1157
+ .option('--no-claude-code', 'Use AI directly instead of Claude Code')
1158
+ .option('--reviewer <provider>', 'Enable reviewer with provider')
1159
+ .option('--reviewer-model <model>', 'Reviewer model', 'gpt-4o')
1160
+ .option('-v, --verbose', 'Verbose output')
1161
+ .action(async (goal, options) => {
1162
+ console.log(banner);
1163
+ console.log(chalk.bold.blue('🚀 Running Goal\n'));
1164
+
1165
+ const spinner = ora('Initializing AI orchestrator...').start();
1166
+
1167
+ try {
1168
+ const { AIOrchestrator } = await import('../server/ai/orchestrator.mjs');
1169
+
1170
+ const orchestrator = new AIOrchestrator({
1171
+ taskManagerProvider: options.taskManager,
1172
+ taskManagerModel: options.taskManagerModel,
1173
+ operatorProvider: options.operator,
1174
+ operatorModel: options.operatorModel,
1175
+ useClaudeCode: options.claudeCode !== false,
1176
+ enableReviewer: !!options.reviewer,
1177
+ reviewerProvider: options.reviewer,
1178
+ reviewerModel: options.reviewerModel,
1179
+ });
1180
+
1181
+ orchestrator.on('initialized', () => spinner.succeed('AI orchestrator initialized'));
1182
+ orchestrator.on('phase:planning', () => spinner.start('Planning tasks...'));
1183
+ orchestrator.on('phase:planned', ({ tasks }) => {
1184
+ spinner.succeed(`Planned ${tasks.length} tasks`);
1185
+ console.log(chalk.dim('\nTasks:'));
1186
+ tasks.forEach((t, i) => console.log(chalk.dim(` ${i + 1}. ${t.title}`)));
1187
+ console.log('');
1188
+ });
1189
+ orchestrator.on('task:started', ({ task }) => spinner.start(`Executing: ${task.title}`));
1190
+ orchestrator.on('task:output', ({ chunk }) => {
1191
+ if (options.verbose) process.stdout.write(chalk.dim(chunk));
1192
+ });
1193
+ orchestrator.on('task:completed', ({ task }) => spinner.succeed(`Completed: ${task.title}`));
1194
+ orchestrator.on('task:failed', ({ task, error }) => spinner.fail(`Failed: ${task.title} - ${error}`));
1195
+ orchestrator.on('goal:completed', ({ completed, failed, total }) => {
1196
+ console.log('');
1197
+ console.log(chalk.bold.green('✅ Goal completed!'));
1198
+ console.log(chalk.dim(` Completed: ${completed}/${total}`));
1199
+ if (failed > 0) console.log(chalk.dim(` Failed: ${failed}`));
1200
+ });
1201
+
1202
+ await orchestrator.initialize();
1203
+ orchestrator.setProject(path.resolve(options.project));
1204
+ await orchestrator.executeGoal(goal);
1205
+
1206
+ } catch (error) {
1207
+ spinner.fail(`Error: ${error.message}`);
1208
+ process.exit(1);
1209
+ }
1210
+ });
1211
+
1212
+ // Plan only
1213
+ program
1214
+ .command('plan <goal>')
1215
+ .description('Plan tasks for a goal without executing')
1216
+ .option('-p, --project <path>', 'Project path', process.cwd())
1217
+ .option('--provider <provider>', 'AI provider', 'anthropic')
1218
+ .option('--model <model>', 'AI model', 'claude-sonnet-4-20250514')
1219
+ .option('-o, --output <file>', 'Output plan to file')
1220
+ .action(async (goal, options) => {
1221
+ console.log(banner);
1222
+ console.log(chalk.bold.blue('📋 Planning: ') + goal + '\n');
1223
+
1224
+ const spinner = ora('Analyzing project and planning...').start();
1225
+
1226
+ try {
1227
+ const { AIOrchestrator } = await import('../server/ai/orchestrator.mjs');
1228
+
1229
+ const orchestrator = new AIOrchestrator({
1230
+ taskManagerProvider: options.provider,
1231
+ taskManagerModel: options.model,
1232
+ });
1233
+
1234
+ await orchestrator.initialize();
1235
+ orchestrator.setProject(path.resolve(options.project));
1236
+
1237
+ const tasks = await orchestrator.planGoal(goal);
1238
+ spinner.succeed(`Planned ${tasks.length} tasks\n`);
1239
+
1240
+ tasks.forEach((task, i) => {
1241
+ console.log(chalk.bold(`${i + 1}. ${task.title}`));
1242
+ console.log(chalk.dim(` Type: ${task.type} | Complexity: ${task.complexity}`));
1243
+ console.log(chalk.dim(` ${task.description}`));
1244
+ if (task.acceptanceCriteria?.length) {
1245
+ console.log(chalk.dim(' Acceptance Criteria:'));
1246
+ task.acceptanceCriteria.forEach(c => console.log(chalk.dim(` - ${c}`)));
1247
+ }
1248
+ console.log('');
1249
+ });
1250
+
1251
+ if (options.output) {
1252
+ await fs.writeFile(options.output, JSON.stringify(tasks, null, 2));
1253
+ console.log(chalk.green(`Plan saved to ${options.output}`));
1254
+ }
1255
+
1256
+ } catch (error) {
1257
+ spinner.fail(`Error: ${error.message}`);
1258
+ process.exit(1);
1259
+ }
1260
+ });
1261
+
1262
+ // ============================================
1263
+ // ARCHITECTURE COMMANDS (from Archie)
1264
+ // ============================================
1265
+
1266
+ function getApiKey() {
1267
+ const apiKey = process.env.ANTHROPIC_API_KEY;
1268
+ if (!apiKey) {
1269
+ console.error(chalk.red('\nError: ANTHROPIC_API_KEY environment variable is not set.\n'));
1270
+ console.log('Set it with:');
1271
+ console.log(chalk.cyan(' export ANTHROPIC_API_KEY=your-api-key\n'));
1272
+ process.exit(1);
1273
+ }
1274
+ return apiKey;
1275
+ }
1276
+
1277
+ // Mermaid diagram utilities
1278
+ function getMermaidLiveUrl(mermaidCode) {
1279
+ const state = {
1280
+ code: mermaidCode.trim(),
1281
+ mermaid: { theme: 'default' },
1282
+ autoSync: true,
1283
+ updateDiagram: true,
1284
+ };
1285
+ const json = JSON.stringify(state);
1286
+ const base64 = Buffer.from(json).toString('base64');
1287
+ return `https://mermaid.live/edit#base64:${base64}`;
1288
+ }
1289
+
1290
+ function extractMermaidDiagrams(markdown) {
1291
+ const diagrams = [];
1292
+ const mermaidRegex = /```mermaid\n([\s\S]*?)```/g;
1293
+ let match;
1294
+
1295
+ while ((match = mermaidRegex.exec(markdown)) !== null) {
1296
+ const code = match[1].trim();
1297
+ const beforeMatch = markdown.slice(0, match.index);
1298
+ const headingMatch = beforeMatch.match(/###?\s+([^\n]+)\n*$/);
1299
+ const title = headingMatch ? headingMatch[1].trim() : undefined;
1300
+ diagrams.push({ code, title });
1301
+ }
1302
+
1303
+ return diagrams;
1304
+ }
1305
+
1306
+ async function openDiagramInBrowser(mermaidCode, title = 'Architecture Diagram') {
1307
+ const html = `<!DOCTYPE html>
1308
+ <html lang="en">
1309
+ <head>
1310
+ <meta charset="UTF-8">
1311
+ <title>${title}</title>
1312
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
1313
+ <style>
1314
+ body { font-family: system-ui; background: #1a1a2e; color: #eee; margin: 0; padding: 20px; min-height: 100vh; }
1315
+ .container { max-width: 1200px; margin: 0 auto; }
1316
+ h1 { color: #00d9ff; }
1317
+ .diagram-container { background: #16213e; border-radius: 12px; padding: 30px; }
1318
+ .mermaid { display: flex; justify-content: center; }
1319
+ </style>
1320
+ </head>
1321
+ <body>
1322
+ <div class="container">
1323
+ <h1>🏗️ ${title}</h1>
1324
+ <div class="diagram-container">
1325
+ <div class="mermaid">${mermaidCode}</div>
1326
+ </div>
1327
+ </div>
1328
+ <script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
1329
+ </body>
1330
+ </html>`;
1331
+
1332
+ const tempFile = `/tmp/ramp-diagram-${Date.now()}.html`;
1333
+ await fs.writeFile(tempFile, html);
1334
+
1335
+ const command = process.platform === 'darwin' ? `open "${tempFile}"` :
1336
+ process.platform === 'win32' ? `start "${tempFile}"` : `xdg-open "${tempFile}"`;
1337
+
1338
+ await execAsync(command);
1339
+ }
1340
+
1341
+ // Simple Architect class
1342
+ class Architect {
1343
+ constructor(apiKey) {
1344
+ this.apiKey = apiKey;
1345
+ this.history = [];
1346
+ }
1347
+
1348
+ async chat(userMessage) {
1349
+ const Anthropic = (await import('@anthropic-ai/sdk')).default;
1350
+ const client = new Anthropic({ apiKey: this.apiKey });
1351
+
1352
+ this.history.push({ role: 'user', content: userMessage });
1353
+
1354
+ const response = await client.messages.create({
1355
+ model: 'claude-sonnet-4-20250514',
1356
+ max_tokens: 8192,
1357
+ system: `You are Ramp Architect, an expert software architect. You help developers design systems with:
1358
+ - Clear tech stack recommendations
1359
+ - Architecture diagrams (Mermaid format)
1360
+ - Project structure
1361
+ - Trade-off analysis
1362
+ - Cost estimates
1363
+ Be opinionated and specific. Give clear paths forward.`,
1364
+ messages: this.history,
1365
+ });
1366
+
1367
+ const assistantMessage = response.content[0].type === 'text' ? response.content[0].text : '';
1368
+ this.history.push({ role: 'assistant', content: assistantMessage });
1369
+ return assistantMessage;
1370
+ }
1371
+
1372
+ clearHistory() {
1373
+ this.history = [];
1374
+ }
1375
+ }
1376
+
1377
+ // Design command
1378
+ program
1379
+ .command('design <description>')
1380
+ .description('Get architecture recommendation for a product idea')
1381
+ .option('-o, --output <file>', 'Save recommendation to file')
1382
+ .action(async (description, options) => {
1383
+ console.log(banner);
1384
+ console.log(chalk.bold.blue('🏗️ Designing Architecture\n'));
1385
+
1386
+ const apiKey = getApiKey();
1387
+ const architect = new Architect(apiKey);
1388
+
1389
+ const spinner = ora('Analyzing your product idea...').start();
1390
+
1391
+ try {
1392
+ const prompt = `I want to build: ${description}
1393
+
1394
+ Please provide a complete architecture recommendation including:
1395
+ 1. Tech stack recommendation with rationale
1396
+ 2. Architecture diagram (Mermaid)
1397
+ 3. Project structure
1398
+ 4. Key decisions and trade-offs
1399
+ 5. Getting started steps
1400
+ 6. Cost estimates
1401
+
1402
+ Be specific and opinionated.`;
1403
+
1404
+ const response = await architect.chat(prompt);
1405
+ spinner.succeed('Architecture recommendation ready!');
1406
+
1407
+ console.log(chalk.cyan('\n─── Architecture Recommendation ───\n'));
1408
+ console.log(response);
1409
+ console.log(chalk.cyan('\n───────────────────────────────────\n'));
1410
+
1411
+ if (options.output) {
1412
+ await fs.writeFile(options.output, response);
1413
+ console.log(chalk.green(`✓ Saved to ${options.output}`));
1414
+ }
1415
+
1416
+ // Check for diagrams
1417
+ const foundDiagrams = extractMermaidDiagrams(response);
1418
+ if (foundDiagrams.length > 0) {
1419
+ const { wantDiagrams } = await inquirer.prompt([{
1420
+ type: 'confirm',
1421
+ name: 'wantDiagrams',
1422
+ message: `Found ${foundDiagrams.length} diagram(s). Open in browser?`,
1423
+ default: true,
1424
+ }]);
1425
+
1426
+ if (wantDiagrams) {
1427
+ for (const diagram of foundDiagrams) {
1428
+ await openDiagramInBrowser(diagram.code, diagram.title || 'Architecture Diagram');
1429
+ }
1430
+ console.log(chalk.green('✓ Diagrams opened\n'));
1431
+ }
1432
+ }
1433
+
1434
+ } catch (error) {
1435
+ spinner.fail('Error generating architecture');
1436
+ console.error(chalk.red(error.message));
1437
+ process.exit(1);
1438
+ }
1439
+ });
1440
+
1441
+ // Compare command
1442
+ program
1443
+ .command('compare')
1444
+ .description('Compare two architecture approaches')
1445
+ .action(async () => {
1446
+ console.log(banner);
1447
+ console.log(chalk.bold.blue('⚖️ Compare Approaches\n'));
1448
+
1449
+ const apiKey = getApiKey();
1450
+ const architect = new Architect(apiKey);
1451
+
1452
+ const { context, approach1, approach2 } = await inquirer.prompt([
1453
+ { type: 'input', name: 'context', message: 'What are you building?' },
1454
+ { type: 'input', name: 'approach1', message: 'First approach:' },
1455
+ { type: 'input', name: 'approach2', message: 'Second approach:' },
1456
+ ]);
1457
+
1458
+ const spinner = ora('Comparing approaches...').start();
1459
+
1460
+ try {
1461
+ const prompt = `I'm building: ${context}
1462
+
1463
+ Please compare these two approaches:
1464
+ 1. ${approach1}
1465
+ 2. ${approach2}
1466
+
1467
+ Provide a detailed comparison with:
1468
+ - Pros and cons of each
1469
+ - Performance implications
1470
+ - Development speed
1471
+ - Maintenance burden
1472
+ - Cost considerations
1473
+ - Clear recommendation with reasoning`;
1474
+
1475
+ const response = await architect.chat(prompt);
1476
+ spinner.succeed('Comparison ready!');
1477
+
1478
+ console.log(chalk.cyan('\n─── Comparison ───\n'));
1479
+ console.log(response);
1480
+ console.log(chalk.cyan('\n──────────────────\n'));
1481
+
1482
+ } catch (error) {
1483
+ spinner.fail('Error comparing approaches');
1484
+ console.error(chalk.red(error.message));
1485
+ }
1486
+ });
1487
+
1488
+ // Interactive architect mode
1489
+ program
1490
+ .command('architect')
1491
+ .alias('arch')
1492
+ .description('Interactive architecture advisor chat')
1493
+ .action(async () => {
1494
+ console.log(banner);
1495
+ console.log(chalk.bold.blue('🏗️ Architecture Advisor\n'));
1496
+
1497
+ const apiKey = getApiKey();
1498
+ const architect = new Architect(apiKey);
1499
+
1500
+ console.log(chalk.gray('Describe your product idea or ask architecture questions.'));
1501
+ console.log(chalk.gray('Commands: /diagram, /clear, /exit\n'));
1502
+
1503
+ let lastResponse = '';
1504
+
1505
+ while (true) {
1506
+ const { input } = await inquirer.prompt([{
1507
+ type: 'input',
1508
+ name: 'input',
1509
+ message: chalk.green('You:'),
1510
+ prefix: '',
1511
+ }]);
1512
+
1513
+ if (!input.trim()) continue;
1514
+
1515
+ if (input.startsWith('/')) {
1516
+ const command = input.slice(1).toLowerCase().split(' ')[0];
1517
+
1518
+ if (command === 'exit' || command === 'quit' || command === 'q') {
1519
+ console.log(chalk.cyan('\nHappy building! 🚀\n'));
1520
+ process.exit(0);
1521
+ }
1522
+
1523
+ if (command === 'clear') {
1524
+ architect.clearHistory();
1525
+ console.log(chalk.yellow('\n✓ Conversation cleared\n'));
1526
+ continue;
1527
+ }
1528
+
1529
+ if (command === 'diagram' || command === 'd') {
1530
+ if (!lastResponse) {
1531
+ console.log(chalk.yellow('\nNo diagrams yet. Describe your product first.\n'));
1532
+ continue;
1533
+ }
1534
+
1535
+ const diagrams = extractMermaidDiagrams(lastResponse);
1536
+ if (diagrams.length === 0) {
1537
+ console.log(chalk.yellow('\nNo Mermaid diagrams found.\n'));
1538
+ continue;
1539
+ }
1540
+
1541
+ console.log(chalk.cyan(`\nOpening ${diagrams.length} diagram(s)...\n`));
1542
+ for (const diagram of diagrams) {
1543
+ await openDiagramInBrowser(diagram.code, diagram.title);
1544
+ }
1545
+ continue;
1546
+ }
1547
+
1548
+ console.log(chalk.yellow(`\nUnknown command: ${command}\n`));
1549
+ continue;
1550
+ }
1551
+
1552
+ // Check entitlements before AI call
1553
+ const idempotencyKey = `architect-${Date.now()}`;
1554
+ const entitlementCheck = await checkAndBurnTokens('architect', idempotencyKey);
1555
+ if (!entitlementCheck.allowed) {
1556
+ console.log(chalk.red(`\n❌ ${entitlementCheck.reason}\n`));
1557
+ continue;
1558
+ }
1559
+
1560
+ const spinner = ora('Thinking...').start();
1561
+ try {
1562
+ const response = await architect.chat(input);
1563
+ spinner.stop();
1564
+ lastResponse = response;
1565
+
1566
+ console.log(chalk.cyan('\n─── Ramp ───\n'));
1567
+ console.log(response);
1568
+ console.log(chalk.cyan('\n─────────────\n'));
1569
+
1570
+ const foundDiagrams = extractMermaidDiagrams(response);
1571
+ if (foundDiagrams.length > 0) {
1572
+ console.log(chalk.gray(`💡 Found ${foundDiagrams.length} diagram(s). Use /diagram to view.\n`));
1573
+ }
1574
+ } catch (error) {
1575
+ spinner.fail('Error');
1576
+ console.error(chalk.red(error.message));
1577
+ }
1578
+ }
1579
+ });
1580
+
1581
+ // ============================================
1582
+ // AUTH COMMANDS
1583
+ // ============================================
1584
+
1585
+ program
1586
+ .command('login')
1587
+ .description('Login to Ramp with your account')
1588
+ .action(async () => {
1589
+ console.log(banner);
1590
+ console.log(chalk.bold.blue('🔐 Login to Ramp\n'));
1591
+
1592
+ // Check if already logged in
1593
+ const existingUser = await getUserInfo();
1594
+ if (existingUser) {
1595
+ console.log(chalk.yellow(`Already logged in as ${existingUser.email}`));
1596
+ const { confirm } = await inquirer.prompt([{
1597
+ type: 'confirm',
1598
+ name: 'confirm',
1599
+ message: 'Login with a different account?',
1600
+ default: false,
1601
+ }]);
1602
+ if (!confirm) return;
1603
+ }
1604
+
1605
+ try {
1606
+ const user = await loginWithBrowser();
1607
+ console.log(chalk.green(`\n✓ Logged in as ${user.email}\n`));
1608
+ console.log(chalk.gray('Your credentials are saved at ~/.ramp/credentials.json\n'));
1609
+ } catch (error) {
1610
+ console.error(chalk.red(`\nLogin failed: ${error.message}\n`));
1611
+ process.exit(1);
1612
+ }
1613
+ });
1614
+
1615
+ program
1616
+ .command('logout')
1617
+ .description('Logout from Ramp')
1618
+ .action(async () => {
1619
+ console.log(banner);
1620
+
1621
+ const user = await getUserInfo();
1622
+ if (!user) {
1623
+ console.log(chalk.yellow('Not currently logged in.\n'));
1624
+ return;
1625
+ }
1626
+
1627
+ await clearCredentials();
1628
+ console.log(chalk.green(`✓ Logged out from ${user.email}\n`));
1629
+ });
1630
+
1631
+ program
1632
+ .command('whoami')
1633
+ .description('Show current logged in user')
1634
+ .action(async () => {
1635
+ const user = await getUserInfo();
1636
+
1637
+ if (!user) {
1638
+ console.log(chalk.yellow('Not logged in. Run `ramplogin` to authenticate.\n'));
1639
+ return;
1640
+ }
1641
+
1642
+ console.log(chalk.cyan('\n Logged in as:'));
1643
+ console.log(` Email: ${chalk.bold(user.email)}`);
1644
+ if (user.displayName) {
1645
+ console.log(` Name: ${user.displayName}`);
1646
+ }
1647
+ console.log(` UID: ${chalk.gray(user.uid)}\n`);
1648
+ });
1649
+
1650
+ // ============================================
1651
+ // CREDITS COMMAND
1652
+ // ============================================
1653
+
1654
+ program
1655
+ .command('credits')
1656
+ .description('Check your AI credits balance')
1657
+ .action(async () => {
1658
+ console.log(banner);
1659
+ console.log(chalk.bold.blue('💳 Credit Balance\n'));
1660
+
1661
+ const spinner = ora('Checking balance...').start();
1662
+
1663
+ try {
1664
+ const balance = await getTokenBalance();
1665
+
1666
+ if (!balance) {
1667
+ spinner.warn('Not logged in to entitlement service');
1668
+ console.log(chalk.gray('\nSet RAMP_TOKEN with your Firebase ID token to check balance.\n'));
1669
+ return;
1670
+ }
1671
+
1672
+ spinner.succeed('Balance retrieved!\n');
1673
+
1674
+ for (const token of balance.balances || []) {
1675
+ console.log(chalk.cyan(` ${token.tokenType}:`));
1676
+ console.log(` Balance: ${chalk.bold(token.balance)}`);
1677
+ console.log(` Used: ${token.lifetimeUsed}`);
1678
+ console.log(` Granted: ${token.lifetimeGranted}\n`);
1679
+ }
1680
+
1681
+ console.log(chalk.gray('Visit rampup.dev to purchase more credits.\n'));
1682
+ } catch (error) {
1683
+ spinner.fail('Failed to check balance');
1684
+ console.error(chalk.red(error.message));
1685
+ }
1686
+ });
1687
+
1688
+ // ============================================
1689
+ // TEMPLATE COMMANDS
1690
+ // ============================================
1691
+
1692
+ program
1693
+ .command('create <name>')
1694
+ .description('Create a new project from template')
1695
+ .option('-t, --template <template>', 'Template to use', 'saas-starter')
1696
+ .option('-d, --dir <directory>', 'Output directory', '.')
1697
+ .action(async (name, options) => {
1698
+ console.log(banner);
1699
+ console.log(chalk.bold.blue(`📦 Creating project: ${name}\n`));
1700
+
1701
+ const spinner = ora('Initializing template...').start();
1702
+
1703
+ try {
1704
+ const templatePath = path.join(__dirname, '..', 'templates', options.template);
1705
+ const outputPath = path.join(path.resolve(options.dir), name);
1706
+
1707
+ try {
1708
+ await fs.access(templatePath);
1709
+ } catch {
1710
+ spinner.fail(`Template not found: ${options.template}`);
1711
+ const templates = await fs.readdir(path.join(__dirname, '..', 'templates'));
1712
+ console.log(chalk.dim('\nAvailable templates:'));
1713
+ templates.forEach(t => console.log(chalk.dim(` - ${t}`)));
1714
+ return;
1715
+ }
1716
+
1717
+ spinner.text = 'Copying template files...';
1718
+ await copyDir(templatePath, outputPath);
1719
+
1720
+ const pkgPath = path.join(outputPath, 'package.json');
1721
+ try {
1722
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf-8'));
1723
+ pkg.name = name;
1724
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
1725
+ } catch {}
1726
+
1727
+ spinner.succeed(`Project created at ${outputPath}`);
1728
+
1729
+ console.log(chalk.dim('\nNext steps:'));
1730
+ console.log(chalk.dim(` cd ${name}`));
1731
+ console.log(chalk.dim(' npm install'));
1732
+ console.log(chalk.dim(' npm run dev\n'));
1733
+
1734
+ } catch (error) {
1735
+ spinner.fail(`Error: ${error.message}`);
1736
+ }
1737
+ });
1738
+
1739
+ program
1740
+ .command('templates')
1741
+ .description('List available project templates')
1742
+ .action(async () => {
1743
+ console.log(banner);
1744
+ console.log(chalk.bold.blue('📁 Available Templates\n'));
1745
+
1746
+ try {
1747
+ const templatesDir = path.join(__dirname, '..', 'templates');
1748
+ const templates = await fs.readdir(templatesDir);
1749
+
1750
+ for (const template of templates) {
1751
+ const infoPath = path.join(templatesDir, template, 'template.json');
1752
+ try {
1753
+ const info = JSON.parse(await fs.readFile(infoPath, 'utf-8'));
1754
+ console.log(chalk.bold(`${template}`));
1755
+ console.log(chalk.dim(` ${info.description}`));
1756
+ } catch {
1757
+ console.log(chalk.bold(`${template}`));
1758
+ }
1759
+ console.log('');
1760
+ }
1761
+ } catch {
1762
+ console.log(chalk.dim('No templates found'));
1763
+ }
1764
+ });
1765
+
1766
+ // ============================================
1767
+ // OMNI - AI Context Bridge Commands
1768
+ // ============================================
1769
+
1770
+ // Context store for Omni
1771
+ class ContextStore {
1772
+ constructor() {
1773
+ this.currentContext = null;
1774
+ this.history = {};
1775
+ }
1776
+
1777
+ async init() {
1778
+ try {
1779
+ const data = await fs.readFile(omniConfig.contextStorePath, 'utf8');
1780
+ this.history = JSON.parse(data);
1781
+ } catch {
1782
+ // No existing contexts
1783
+ }
1784
+ }
1785
+
1786
+ async save() {
1787
+ const dir = path.dirname(omniConfig.contextStorePath);
1788
+ await fs.mkdir(dir, { recursive: true });
1789
+ await fs.writeFile(omniConfig.contextStorePath, JSON.stringify(this.history, null, 2));
1790
+ }
1791
+
1792
+ async setContext(name) {
1793
+ this.currentContext = name;
1794
+ if (!this.history[name]) {
1795
+ this.history[name] = { messages: [], lastAccessed: new Date().toISOString() };
1796
+ } else {
1797
+ this.history[name].lastAccessed = new Date().toISOString();
1798
+ }
1799
+ await this.save();
1800
+ return this.history[name];
1801
+ }
1802
+
1803
+ async addMessage(role, content, source) {
1804
+ if (!this.currentContext) throw new Error('No active context');
1805
+ this.history[this.currentContext].messages.push({
1806
+ role, content, source, timestamp: new Date().toISOString()
1807
+ });
1808
+ await this.save();
1809
+ }
1810
+
1811
+ getCurrent() {
1812
+ if (!this.currentContext) return null;
1813
+ return { name: this.currentContext, ...this.history[this.currentContext] };
1814
+ }
1815
+
1816
+ list() {
1817
+ return Object.keys(this.history).map(name => ({
1818
+ name,
1819
+ messages: this.history[name].messages.length,
1820
+ lastAccessed: this.history[name].lastAccessed
1821
+ }));
1822
+ }
1823
+ }
1824
+
1825
+ function formatContextForSharing(context) {
1826
+ if (!context?.messages?.length) return 'No previous conversation context.';
1827
+
1828
+ const lines = [
1829
+ '### Conversation Context ###',
1830
+ 'Sharing context from a previous conversation:',
1831
+ ''
1832
+ ];
1833
+
1834
+ const recent = context.messages.slice(-10);
1835
+ recent.forEach((msg, i) => {
1836
+ const role = msg.role === 'user' ? 'Human' : 'AI';
1837
+ const source = msg.source ? ` (${msg.source})` : '';
1838
+ lines.push(`${role}${source}: ${msg.content.slice(0, 300)}${msg.content.length > 300 ? '...' : ''}`);
1839
+ if (i < recent.length - 1) lines.push('');
1840
+ });
1841
+
1842
+ lines.push('', 'Please continue with this context in mind.');
1843
+ return lines.join('\n');
1844
+ }
1845
+
1846
+ // Omni interactive mode
1847
+ async function omniInteractive() {
1848
+ const store = new ContextStore();
1849
+ await store.init();
1850
+
1851
+ console.log(chalk.cyan('\n─── Omni - AI Context Bridge ───\n'));
1852
+ console.log(chalk.gray('Bridge conversations between AI applications\n'));
1853
+
1854
+ // Select or create context
1855
+ const contexts = store.list();
1856
+ let contextName;
1857
+
1858
+ if (contexts.length > 0) {
1859
+ console.log(chalk.dim('Existing contexts:'));
1860
+ contexts.forEach((c, i) => console.log(chalk.dim(` ${i + 1}. ${c.name} (${c.messages} msgs)`)));
1861
+ console.log('');
1862
+ }
1863
+
1864
+ const { action } = await inquirer.prompt([{
1865
+ type: 'list',
1866
+ name: 'action',
1867
+ message: 'Select or create context:',
1868
+ choices: [
1869
+ ...contexts.map(c => ({ name: `${c.name} (${c.messages} messages)`, value: c.name })),
1870
+ { name: '+ Create new context', value: '_new' }
1871
+ ]
1872
+ }]);
1873
+
1874
+ if (action === '_new') {
1875
+ const { name } = await inquirer.prompt([{
1876
+ type: 'input',
1877
+ name: 'name',
1878
+ message: 'Context name:',
1879
+ default: `context-${Date.now()}`
1880
+ }]);
1881
+ contextName = name;
1882
+ } else {
1883
+ contextName = action;
1884
+ }
1885
+
1886
+ await store.setContext(contextName);
1887
+ console.log(chalk.green(`\n✓ Active context: ${contextName}\n`));
1888
+
1889
+ // Main loop
1890
+ while (true) {
1891
+ const current = store.getCurrent();
1892
+ const { choice } = await inquirer.prompt([{
1893
+ type: 'list',
1894
+ name: 'choice',
1895
+ message: `Context: ${current.name} (${current.messages.length} msgs) - What to do?`,
1896
+ choices: [
1897
+ { name: '📋 Copy context to clipboard', value: 'copy' },
1898
+ { name: '🚀 Open Claude app', value: 'claude' },
1899
+ { name: '💬 Open ChatGPT app', value: 'chatgpt' },
1900
+ { name: '➕ Add message to context', value: 'add' },
1901
+ { name: '👁️ View context', value: 'view' },
1902
+ { name: '🔄 Switch context', value: 'switch' },
1903
+ { name: '🚪 Exit', value: 'exit' }
1904
+ ]
1905
+ }]);
1906
+
1907
+ switch (choice) {
1908
+ case 'copy': {
1909
+ const formatted = formatContextForSharing(current);
1910
+ const clipboardy = (await import('clipboardy')).default;
1911
+ await clipboardy.write(formatted);
1912
+ console.log(chalk.green('✓ Context copied to clipboard\n'));
1913
+ break;
1914
+ }
1915
+
1916
+ case 'claude':
1917
+ case 'chatgpt': {
1918
+ const formatted = formatContextForSharing(current);
1919
+ const clipboardy = (await import('clipboardy')).default;
1920
+ await clipboardy.write(formatted);
1921
+
1922
+ const appPath = choice === 'claude' ? omniConfig.paths.claude : omniConfig.paths.chatGpt;
1923
+ const open = (await import('open')).default;
1924
+
1925
+ console.log(chalk.cyan(`Opening ${choice}...`));
1926
+ await open(appPath);
1927
+ console.log(chalk.green('✓ Context copied. Paste into the app.\n'));
1928
+ break;
1929
+ }
1930
+
1931
+ case 'add': {
1932
+ const { role, content } = await inquirer.prompt([
1933
+ {
1934
+ type: 'list',
1935
+ name: 'role',
1936
+ message: 'Message role:',
1937
+ choices: [
1938
+ { name: 'User (human)', value: 'user' },
1939
+ { name: 'Assistant (AI)', value: 'assistant' }
1940
+ ]
1941
+ },
1942
+ {
1943
+ type: 'input',
1944
+ name: 'content',
1945
+ message: 'Message content:'
1946
+ }
1947
+ ]);
1948
+ await store.addMessage(role, content, 'manual');
1949
+ console.log(chalk.green('✓ Message added\n'));
1950
+ break;
1951
+ }
1952
+
1953
+ case 'view':
1954
+ console.log(chalk.cyan('\n─── Context Messages ───\n'));
1955
+ if (current.messages.length === 0) {
1956
+ console.log(chalk.dim('No messages yet.\n'));
1957
+ } else {
1958
+ current.messages.forEach((msg, i) => {
1959
+ const role = msg.role === 'user' ? chalk.blue('Human') : chalk.green('AI');
1960
+ const source = msg.source ? chalk.dim(` (${msg.source})`) : '';
1961
+ console.log(`${i + 1}. ${role}${source}: ${msg.content.slice(0, 100)}${msg.content.length > 100 ? '...' : ''}`);
1962
+ });
1963
+ console.log('');
1964
+ }
1965
+ break;
1966
+
1967
+ case 'switch': {
1968
+ const contexts = store.list();
1969
+ const { newCtx } = await inquirer.prompt([{
1970
+ type: 'list',
1971
+ name: 'newCtx',
1972
+ message: 'Select context:',
1973
+ choices: [
1974
+ ...contexts.map(c => ({ name: `${c.name} (${c.messages} msgs)`, value: c.name })),
1975
+ { name: '+ Create new', value: '_new' }
1976
+ ]
1977
+ }]);
1978
+
1979
+ if (newCtx === '_new') {
1980
+ const { name } = await inquirer.prompt([{ type: 'input', name: 'name', message: 'Context name:' }]);
1981
+ await store.setContext(name);
1982
+ } else {
1983
+ await store.setContext(newCtx);
1984
+ }
1985
+ console.log(chalk.green(`✓ Switched to: ${store.currentContext}\n`));
1986
+ break;
1987
+ }
1988
+
1989
+ case 'exit':
1990
+ console.log(chalk.cyan('\nGoodbye!\n'));
1991
+ process.exit(0);
1992
+ }
1993
+ }
1994
+ }
1995
+
1996
+ // Omni command
1997
+ program
1998
+ .command('omni')
1999
+ .description('AI Context Bridge - share context between AI apps')
2000
+ .action(omniInteractive);
2001
+
2002
+ // Omni list contexts
2003
+ program
2004
+ .command('omni:list')
2005
+ .description('List all saved conversation contexts')
2006
+ .action(async () => {
2007
+ console.log(banner);
2008
+ console.log(chalk.bold.blue('📋 Saved Contexts\n'));
2009
+
2010
+ const store = new ContextStore();
2011
+ await store.init();
2012
+ const contexts = store.list();
2013
+
2014
+ if (contexts.length === 0) {
2015
+ console.log(chalk.dim('No contexts found. Use "ramp omni" to create one.\n'));
2016
+ } else {
2017
+ contexts.forEach(c => {
2018
+ console.log(chalk.bold(c.name));
2019
+ console.log(chalk.dim(` Messages: ${c.messages}`));
2020
+ console.log(chalk.dim(` Last used: ${new Date(c.lastAccessed).toLocaleString()}`));
2021
+ console.log('');
2022
+ });
2023
+ }
2024
+ });
2025
+
2026
+ // Omni send context
2027
+ program
2028
+ .command('omni:send <context> <target>')
2029
+ .description('Send context to AI app (claude|chatgpt)')
2030
+ .action(async (contextName, target) => {
2031
+ console.log(banner);
2032
+
2033
+ const store = new ContextStore();
2034
+ await store.init();
2035
+
2036
+ if (!store.history[contextName]) {
2037
+ console.error(chalk.red(`Context "${contextName}" not found.`));
2038
+ process.exit(1);
2039
+ }
2040
+
2041
+ await store.setContext(contextName);
2042
+ const current = store.getCurrent();
2043
+ const formatted = formatContextForSharing(current);
2044
+
2045
+ const clipboardy = (await import('clipboardy')).default;
2046
+ await clipboardy.write(formatted);
2047
+
2048
+ const targetLower = target.toLowerCase();
2049
+ if (targetLower !== 'claude' && targetLower !== 'chatgpt') {
2050
+ console.error(chalk.red('Target must be "claude" or "chatgpt"'));
2051
+ process.exit(1);
2052
+ }
2053
+
2054
+ const appPath = targetLower === 'claude' ? omniConfig.paths.claude : omniConfig.paths.chatGpt;
2055
+ const open = (await import('open')).default;
2056
+
2057
+ console.log(chalk.cyan(`Opening ${target}...`));
2058
+ await open(appPath);
2059
+ console.log(chalk.green('✓ Context copied to clipboard. Paste into the app.\n'));
2060
+ });
2061
+
2062
+ // ============================================
2063
+ // UTILITY COMMANDS
2064
+ // ============================================
2065
+
2066
+ program
2067
+ .command('models')
2068
+ .description('List available AI models')
2069
+ .action(async () => {
2070
+ console.log(banner);
2071
+ console.log(chalk.bold.blue('📦 Available AI Models\n'));
2072
+
2073
+ try {
2074
+ const { getAvailableProviders } = await import('../server/ai/providers/index.mjs');
2075
+ const providers = getAvailableProviders();
2076
+
2077
+ providers.forEach(provider => {
2078
+ console.log(chalk.bold(`${provider.displayName}:`));
2079
+ provider.models.forEach(model => {
2080
+ console.log(chalk.dim(` ${model.id} - ${model.name}`));
2081
+ });
2082
+ console.log('');
2083
+ });
2084
+ } catch {
2085
+ console.log(chalk.dim('Anthropic Claude models:'));
2086
+ console.log(chalk.dim(' claude-sonnet-4-20250514'));
2087
+ console.log(chalk.dim(' claude-opus-4-20250514'));
2088
+ }
2089
+ });
2090
+
2091
+ // Interactive mode (default)
2092
+ program
2093
+ .command('interactive')
2094
+ .alias('i')
2095
+ .description('Interactive mode')
2096
+ .action(async () => {
2097
+ console.log(banner);
2098
+
2099
+ const { mode } = await inquirer.prompt([{
2100
+ type: 'list',
2101
+ name: 'mode',
2102
+ message: 'How do you want to learn this codebase?',
2103
+ choices: [
2104
+ { name: '🎤 Voice chat (talk to your code)', value: 'voice' },
2105
+ { name: '💬 Text chat', value: 'learn' },
2106
+ { name: '📁 More options...', value: 'more' },
2107
+ ],
2108
+ }]);
2109
+
2110
+ if (mode === 'voice') {
2111
+ await program.parseAsync(['node', 'ramp', 'voice']);
2112
+ } else if (mode === 'learn') {
2113
+ await program.parseAsync(['node', 'ramp', 'learn']);
2114
+ } else if (mode === 'more') {
2115
+ // Show expanded options
2116
+ const { moreMode } = await inquirer.prompt([{
2117
+ type: 'list',
2118
+ name: 'moreMode',
2119
+ message: 'Choose an option:',
2120
+ choices: [
2121
+ { name: '🚀 Start Onboarding', value: 'start' },
2122
+ { name: '🔍 Explore Codebase', value: 'explore' },
2123
+ { name: '📚 Learn with AI Guide', value: 'learn' },
2124
+ { name: '❓ Ask a Question', value: 'ask' },
2125
+ { name: '📖 Generate Onboarding Doc', value: 'guide' },
2126
+ { name: '📊 View Progress', value: 'progress' },
2127
+ new inquirer.Separator('── More Tools ──'),
2128
+ { name: '🏗️ Design Architecture', value: 'architect' },
2129
+ { name: '🚀 Run AI Goal', value: 'run' },
2130
+ ],
2131
+ }]);
2132
+
2133
+ if (moreMode === 'start') {
2134
+ await program.parseAsync(['node', 'ramp', 'start']);
2135
+ } else if (moreMode === 'explore') {
2136
+ await program.parseAsync(['node', 'ramp', 'explore']);
2137
+ } else if (moreMode === 'learn') {
2138
+ await program.parseAsync(['node', 'ramp', 'learn']);
2139
+ } else if (moreMode === 'ask') {
2140
+ const { question } = await inquirer.prompt([{
2141
+ type: 'input',
2142
+ name: 'question',
2143
+ message: 'What do you want to know?'
2144
+ }]);
2145
+ if (question) {
2146
+ await program.parseAsync(['node', 'ramp', 'ask', question]);
2147
+ }
2148
+ } else if (moreMode === 'guide') {
2149
+ await program.parseAsync(['node', 'ramp', 'guide']);
2150
+ } else if (moreMode === 'progress') {
2151
+ await program.parseAsync(['node', 'ramp', 'progress']);
2152
+ } else if (moreMode === 'architect') {
2153
+ await program.parseAsync(['node', 'ramp', 'architect']);
2154
+ } else {
2155
+ console.log(chalk.dim(`\nRun: ramp ${moreMode} <args>\n`));
2156
+ }
2157
+ }
2158
+ });
2159
+
2160
+ // Helper function
2161
+ async function copyDir(src, dest) {
2162
+ await fs.mkdir(dest, { recursive: true });
2163
+ const entries = await fs.readdir(src, { withFileTypes: true });
2164
+
2165
+ for (const entry of entries) {
2166
+ const srcPath = path.join(src, entry.name);
2167
+ const destPath = path.join(dest, entry.name);
2168
+
2169
+ if (entry.isDirectory()) {
2170
+ await copyDir(srcPath, destPath);
2171
+ } else {
2172
+ await fs.copyFile(srcPath, destPath);
2173
+ }
2174
+ }
2175
+ }
2176
+
2177
+ // ============================================
2178
+ // KNOWLEDGE BASE COMMANDS
2179
+ // ============================================
2180
+
2181
+ // Track last Q&A for saving
2182
+ let lastQA = null;
2183
+
2184
+ // Search team knowledge base
2185
+ program
2186
+ .command('kb [query]')
2187
+ .alias('knowledge')
2188
+ .description('Search your team\'s knowledge base')
2189
+ .option('-r, --repo <url>', 'Filter by repository URL')
2190
+ .option('-c, --category <category>', 'Filter by category (general, architecture, onboarding, debugging)')
2191
+ .option('-n, --limit <number>', 'Max results', '5')
2192
+ .action(async (query, options) => {
2193
+ console.log(banner);
2194
+ console.log(chalk.bold.blue('📚 Team Knowledge Base\n'));
2195
+
2196
+ const spinner = ora('Searching knowledge base...').start();
2197
+
2198
+ try {
2199
+ const org = await getMyOrg();
2200
+ if (!org) {
2201
+ spinner.fail('You are not part of a team');
2202
+ console.log(chalk.dim('\nJoin or create a team at rampup.dev to use the knowledge base.\n'));
2203
+ return;
2204
+ }
2205
+
2206
+ spinner.text = `Searching ${org.name}'s knowledge base...`;
2207
+
2208
+ const result = await searchKnowledge({
2209
+ query: query || '',
2210
+ repoUrl: options.repo,
2211
+ category: options.category,
2212
+ limit: parseInt(options.limit, 10),
2213
+ });
2214
+
2215
+ spinner.stop();
2216
+
2217
+ if (result.items.length === 0) {
2218
+ console.log(chalk.yellow('No entries found.'));
2219
+ if (query) {
2220
+ console.log(chalk.dim(`\nTry a different search or run: rampsave to add new knowledge\n`));
2221
+ } else {
2222
+ console.log(chalk.dim(`\nYour team hasn't saved any knowledge yet. Run: rampsave to add the first entry!\n`));
2223
+ }
2224
+ return;
2225
+ }
2226
+
2227
+ console.log(chalk.bold(`Found ${result.total} entries${query ? ` matching "${query}"` : ''}:\n`));
2228
+
2229
+ for (let i = 0; i < result.items.length; i++) {
2230
+ const entry = result.items[i];
2231
+ console.log(chalk.cyan('─'.repeat(60)));
2232
+ console.log(formatKnowledgeEntry(entry));
2233
+ }
2234
+
2235
+ console.log(chalk.cyan('─'.repeat(60)));
2236
+
2237
+ if (result.hasMore) {
2238
+ console.log(chalk.dim(`\n${result.total - result.items.length} more results. Use --limit to see more.\n`));
2239
+ }
2240
+
2241
+ } catch (error) {
2242
+ spinner.fail('Search failed');
2243
+ console.error(chalk.red(error.message));
2244
+ }
2245
+ });
2246
+
2247
+ // Save to knowledge base
2248
+ program
2249
+ .command('save')
2250
+ .description('Save a Q&A to your team\'s knowledge base')
2251
+ .option('-q, --question <text>', 'The question')
2252
+ .option('-a, --answer <text>', 'The answer')
2253
+ .option('-r, --repo <url>', 'Associated repository URL')
2254
+ .option('-t, --tags <tags>', 'Comma-separated tags')
2255
+ .option('-c, --category <category>', 'Category: general, architecture, onboarding, debugging', 'general')
2256
+ .action(async (options) => {
2257
+ console.log(banner);
2258
+ console.log(chalk.bold.blue('💾 Save to Knowledge Base\n'));
2259
+
2260
+ try {
2261
+ const org = await getMyOrg();
2262
+ if (!org) {
2263
+ console.log(chalk.red('You are not part of a team.'));
2264
+ console.log(chalk.dim('\nJoin or create a team at rampup.dev to save knowledge.\n'));
2265
+ return;
2266
+ }
2267
+
2268
+ let question = options.question;
2269
+ let answer = options.answer;
2270
+
2271
+ // If not provided, prompt interactively
2272
+ if (!question || !answer) {
2273
+ if (lastQA) {
2274
+ console.log(chalk.dim('Last Q&A from this session:'));
2275
+ console.log(chalk.dim(`Q: ${lastQA.question.substring(0, 100)}...`));
2276
+ console.log(chalk.dim(`A: ${lastQA.answer.substring(0, 100)}...`));
2277
+ console.log('');
2278
+
2279
+ const { useLastQA } = await inquirer.prompt([
2280
+ {
2281
+ type: 'confirm',
2282
+ name: 'useLastQA',
2283
+ message: 'Save this Q&A?',
2284
+ default: true,
2285
+ }
2286
+ ]);
2287
+
2288
+ if (useLastQA) {
2289
+ question = lastQA.question;
2290
+ answer = lastQA.answer;
2291
+ }
2292
+ }
2293
+
2294
+ if (!question) {
2295
+ const { inputQuestion } = await inquirer.prompt([
2296
+ {
2297
+ type: 'input',
2298
+ name: 'inputQuestion',
2299
+ message: 'Question:',
2300
+ validate: (input) => input.trim().length > 0 || 'Question is required',
2301
+ }
2302
+ ]);
2303
+ question = inputQuestion;
2304
+ }
2305
+
2306
+ if (!answer) {
2307
+ const { inputAnswer } = await inquirer.prompt([
2308
+ {
2309
+ type: 'editor',
2310
+ name: 'inputAnswer',
2311
+ message: 'Answer (opens editor):',
2312
+ validate: (input) => input.trim().length > 0 || 'Answer is required',
2313
+ }
2314
+ ]);
2315
+ answer = inputAnswer;
2316
+ }
2317
+ }
2318
+
2319
+ // Parse tags
2320
+ const tags = options.tags ? options.tags.split(',').map(t => t.trim()) : [];
2321
+
2322
+ // Confirm before saving
2323
+ console.log(chalk.bold('\nSaving to knowledge base:'));
2324
+ console.log(chalk.cyan(`Category: ${options.category}`));
2325
+ if (tags.length > 0) console.log(chalk.cyan(`Tags: ${tags.join(', ')}`));
2326
+ if (options.repo) console.log(chalk.cyan(`Repo: ${options.repo}`));
2327
+ console.log(chalk.dim(`Q: ${question.substring(0, 80)}${question.length > 80 ? '...' : ''}`));
2328
+
2329
+ const spinner = ora('Saving to knowledge base...').start();
2330
+
2331
+ const entry = await saveKnowledge({
2332
+ question,
2333
+ answer,
2334
+ repoUrl: options.repo,
2335
+ tags,
2336
+ category: options.category,
2337
+ });
2338
+
2339
+ spinner.succeed('Saved successfully!');
2340
+ console.log(chalk.dim(`\nEntry ID: ${entry.id}`));
2341
+ console.log(chalk.dim(`Team members can find this with: rampkb "${question.split(' ').slice(0, 3).join(' ')}..."\n`));
2342
+
2343
+ } catch (error) {
2344
+ console.error(chalk.red(error.message));
2345
+ }
2346
+ });
2347
+
2348
+ // Default to interactive
2349
+ program.action(() => {
2350
+ program.parseAsync(['node', 'ramp', 'interactive']);
2351
+ });
2352
+
2353
+ program.parse();