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/auth.js +259 -0
- package/entitlements.js +123 -0
- package/index.js +2353 -0
- package/knowledge.js +228 -0
- package/omni/config.js +51 -0
- package/package.json +49 -0
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();
|