codebakers 2.0.4 → 2.0.10

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.
@@ -0,0 +1,312 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import Anthropic from '@anthropic-ai/sdk';
4
+ import { Config } from '../utils/config.js';
5
+
6
+ // ============================================================================
7
+ // COMMAND MAPPING
8
+ // ============================================================================
9
+
10
+ interface ParsedCommand {
11
+ command: string;
12
+ args: string[];
13
+ confidence: number;
14
+ interpretation: string;
15
+ }
16
+
17
+ interface CommandOption {
18
+ value: string;
19
+ label: string;
20
+ description: string;
21
+ }
22
+
23
+ const COMMAND_PATTERNS: Record<string, { patterns: string[]; description: string }> = {
24
+ init: {
25
+ patterns: ['new project', 'create app', 'start project', 'initialize', 'new app', 'start building', 'create new'],
26
+ description: 'Create a new project',
27
+ },
28
+ build: {
29
+ patterns: ['build from prd', 'build app', 'build project', 'use prd', 'parallel build', 'swarm build', 'build this'],
30
+ description: 'Build from PRD with parallel agents',
31
+ },
32
+ code: {
33
+ patterns: ['add feature', 'create component', 'fix bug', 'help me code', 'write code', 'implement', 'add', 'create'],
34
+ description: 'AI coding agent',
35
+ },
36
+ check: {
37
+ patterns: ['check code', 'lint', 'validate', 'any problems', 'check quality', 'review code', 'check patterns'],
38
+ description: 'Check code quality and patterns',
39
+ },
40
+ deploy: {
41
+ patterns: ['deploy', 'publish', 'go live', 'push to production', 'ship it', 'release', 'launch'],
42
+ description: 'Deploy to Vercel',
43
+ },
44
+ fix: {
45
+ patterns: ['fix errors', 'fix bugs', 'repair', 'broken', 'not working', 'fix issues', 'auto fix'],
46
+ description: 'Auto-fix errors',
47
+ },
48
+ migrate: {
49
+ patterns: ['database', 'migration', 'schema', 'db push', 'migrate', 'update database'],
50
+ description: 'Database migrations',
51
+ },
52
+ design: {
53
+ patterns: ['design', 'colors', 'style', 'theme', 'branding', 'look and feel', 'ui style'],
54
+ description: 'Design system settings',
55
+ },
56
+ advisors: {
57
+ patterns: ['advisors', 'dream team', 'experts', 'feedback', 'review idea', 'consult', 'get advice'],
58
+ description: 'Dream Team advisory board',
59
+ },
60
+ 'prd-maker': {
61
+ patterns: ['make prd', 'create prd', 'write requirements', 'plan project', 'document requirements', 'spec'],
62
+ description: 'Create PRD through interview',
63
+ },
64
+ security: {
65
+ patterns: ['security', 'vulnerabilities', 'audit', 'security check', 'scan for issues'],
66
+ description: 'Security audit',
67
+ },
68
+ status: {
69
+ patterns: ['status', 'progress', 'how is it going', 'show projects', 'list projects'],
70
+ description: 'Project status',
71
+ },
72
+ setup: {
73
+ patterns: ['setup', 'configure', 'connect accounts', 'api keys', 'settings'],
74
+ description: 'Initial setup',
75
+ },
76
+ help: {
77
+ patterns: ['help', 'what can you do', 'commands', 'how to use', 'guide'],
78
+ description: 'Show help',
79
+ },
80
+ };
81
+
82
+ // ============================================================================
83
+ // NATURAL LANGUAGE PARSER
84
+ // ============================================================================
85
+
86
+ export async function parseNaturalLanguage(input: string, config: Config): Promise<ParsedCommand | null> {
87
+ const normalizedInput = input.toLowerCase().trim();
88
+
89
+ // Quick pattern matching first (no API call needed)
90
+ const quickMatch = quickPatternMatch(normalizedInput);
91
+ if (quickMatch && quickMatch.confidence > 0.8) {
92
+ return quickMatch;
93
+ }
94
+
95
+ // For ambiguous or complex inputs, use Claude
96
+ const anthropicCreds = config.getCredentials('anthropic');
97
+ if (!anthropicCreds?.apiKey) {
98
+ // Fall back to best guess without AI
99
+ return quickMatch;
100
+ }
101
+
102
+ const anthropic = new Anthropic({ apiKey: anthropicCreds.apiKey });
103
+
104
+ try {
105
+ const result = await parseWithAI(anthropic, input);
106
+ return result;
107
+ } catch {
108
+ // Fall back to pattern matching
109
+ return quickMatch;
110
+ }
111
+ }
112
+
113
+ function quickPatternMatch(input: string): ParsedCommand | null {
114
+ let bestMatch: ParsedCommand | null = null;
115
+ let highestScore = 0;
116
+
117
+ for (const [command, { patterns, description }] of Object.entries(COMMAND_PATTERNS)) {
118
+ for (const pattern of patterns) {
119
+ const score = calculateSimilarity(input, pattern);
120
+ if (score > highestScore) {
121
+ highestScore = score;
122
+ bestMatch = {
123
+ command,
124
+ args: extractArgs(input, pattern),
125
+ confidence: score,
126
+ interpretation: description,
127
+ };
128
+ }
129
+ }
130
+ }
131
+
132
+ // Check for direct command names
133
+ for (const command of Object.keys(COMMAND_PATTERNS)) {
134
+ if (input.includes(command)) {
135
+ return {
136
+ command,
137
+ args: extractArgs(input, command),
138
+ confidence: 0.95,
139
+ interpretation: COMMAND_PATTERNS[command].description,
140
+ };
141
+ }
142
+ }
143
+
144
+ return bestMatch;
145
+ }
146
+
147
+ function calculateSimilarity(input: string, pattern: string): number {
148
+ const inputWords = input.toLowerCase().split(/\s+/);
149
+ const patternWords = pattern.toLowerCase().split(/\s+/);
150
+
151
+ let matches = 0;
152
+ for (const patternWord of patternWords) {
153
+ if (inputWords.some(w => w.includes(patternWord) || patternWord.includes(w))) {
154
+ matches++;
155
+ }
156
+ }
157
+
158
+ return matches / patternWords.length;
159
+ }
160
+
161
+ function extractArgs(input: string, pattern: string): string[] {
162
+ // Remove the pattern words to get remaining args
163
+ const patternWords = pattern.toLowerCase().split(/\s+/);
164
+ const inputWords = input.split(/\s+/);
165
+
166
+ const args = inputWords.filter(word =>
167
+ !patternWords.some(p => word.toLowerCase().includes(p) || p.includes(word.toLowerCase()))
168
+ );
169
+
170
+ return args.filter(a => a.length > 2); // Filter out tiny words
171
+ }
172
+
173
+ async function parseWithAI(anthropic: Anthropic, input: string): Promise<ParsedCommand> {
174
+ const commandList = Object.entries(COMMAND_PATTERNS)
175
+ .map(([cmd, { description }]) => `- ${cmd}: ${description}`)
176
+ .join('\n');
177
+
178
+ const response = await anthropic.messages.create({
179
+ model: 'claude-sonnet-4-20250514',
180
+ max_tokens: 500,
181
+ messages: [{
182
+ role: 'user',
183
+ content: `Parse this user input into a CLI command.
184
+
185
+ User said: "${input}"
186
+
187
+ Available commands:
188
+ ${commandList}
189
+ - code: For any coding/feature request not matching above
190
+
191
+ Return JSON only:
192
+ {
193
+ "command": "command-name",
194
+ "args": ["any", "extracted", "arguments"],
195
+ "confidence": 0.95,
196
+ "interpretation": "What the user wants to do"
197
+ }
198
+
199
+ If the input is asking to build/create/add a specific feature, use "code" and put the request in args.
200
+ If unclear between multiple commands, use the most likely one with lower confidence.`
201
+ }],
202
+ });
203
+
204
+ const text = response.content[0].type === 'text' ? response.content[0].text : '';
205
+ const jsonMatch = text.match(/\{[\s\S]*?\}/);
206
+
207
+ if (!jsonMatch) {
208
+ // Default to code command for anything unclear
209
+ return {
210
+ command: 'code',
211
+ args: [input],
212
+ confidence: 0.5,
213
+ interpretation: 'Passing to AI coding agent',
214
+ };
215
+ }
216
+
217
+ return JSON.parse(jsonMatch[0]);
218
+ }
219
+
220
+ // ============================================================================
221
+ // CLARIFICATION
222
+ // ============================================================================
223
+
224
+ export async function clarifyCommand(parsed: ParsedCommand): Promise<ParsedCommand> {
225
+ // If confidence is high enough, no clarification needed
226
+ if (parsed.confidence >= 0.8) {
227
+ return parsed;
228
+ }
229
+
230
+ // If confidence is medium, confirm
231
+ if (parsed.confidence >= 0.5) {
232
+ const confirm = await p.confirm({
233
+ message: `Did you mean: ${parsed.interpretation}?`,
234
+ initialValue: true,
235
+ });
236
+
237
+ if (confirm && !p.isCancel(confirm)) {
238
+ return { ...parsed, confidence: 1 };
239
+ }
240
+ }
241
+
242
+ // Low confidence or user said no - ask for clarification
243
+ const options: CommandOption[] = [
244
+ { value: 'init', label: '🆕 Create new project', description: 'Start a fresh project' },
245
+ { value: 'build', label: '🏗️ Build from PRD', description: 'Parallel build from document' },
246
+ { value: 'code', label: '💬 AI Coding', description: 'Get help writing code' },
247
+ { value: 'check', label: '🔍 Check code', description: 'Lint and validate' },
248
+ { value: 'deploy', label: '🚀 Deploy', description: 'Push to production' },
249
+ { value: 'fix', label: '🔧 Fix errors', description: 'Auto-fix issues' },
250
+ { value: 'advisors', label: '🌟 Dream Team', description: 'Get expert advice' },
251
+ { value: 'prd-maker', label: '📝 Create PRD', description: 'Document your idea' },
252
+ { value: 'other', label: '❓ Something else', description: 'Describe what you need' },
253
+ ];
254
+
255
+ const selection = await p.select({
256
+ message: 'What would you like to do?',
257
+ options: options.map(o => ({ value: o.value, label: o.label, hint: o.description })),
258
+ });
259
+
260
+ if (p.isCancel(selection)) {
261
+ return { ...parsed, command: 'cancel', confidence: 1 };
262
+ }
263
+
264
+ if (selection === 'other') {
265
+ const description = await p.text({
266
+ message: 'Describe what you want to do:',
267
+ placeholder: 'I want to...',
268
+ });
269
+
270
+ if (p.isCancel(description)) {
271
+ return { ...parsed, command: 'cancel', confidence: 1 };
272
+ }
273
+
274
+ // Pass to code agent as free-form request
275
+ return {
276
+ command: 'code',
277
+ args: [description as string],
278
+ confidence: 1,
279
+ interpretation: description as string,
280
+ };
281
+ }
282
+
283
+ return {
284
+ command: selection as string,
285
+ args: parsed.args,
286
+ confidence: 1,
287
+ interpretation: options.find(o => o.value === selection)?.description || '',
288
+ };
289
+ }
290
+
291
+ // ============================================================================
292
+ // DEPLOY CLARIFICATION
293
+ // ============================================================================
294
+
295
+ export async function clarifyDeployTarget(): Promise<'preview' | 'production' | null> {
296
+ const target = await p.select({
297
+ message: 'Where do you want to deploy?',
298
+ options: [
299
+ { value: 'preview', label: '🔍 Preview', hint: 'Test URL to review changes' },
300
+ { value: 'production', label: '🚀 Production', hint: 'Live site for users' },
301
+ ],
302
+ });
303
+
304
+ if (p.isCancel(target)) return null;
305
+ return target as 'preview' | 'production';
306
+ }
307
+
308
+ // ============================================================================
309
+ // EXPORTS
310
+ // ============================================================================
311
+
312
+ export { COMMAND_PATTERNS };
@@ -0,0 +1,323 @@
1
+ import * as p from '@clack/prompts';
2
+ import chalk from 'chalk';
3
+ import * as fs from 'fs-extra';
4
+ import { execa } from 'execa';
5
+
6
+ let voiceAvailable: boolean | null = null;
7
+
8
+ export async function checkVoiceAvailability(): Promise<boolean> {
9
+ if (voiceAvailable !== null) return voiceAvailable;
10
+
11
+ try {
12
+ if (process.platform === 'win32') {
13
+ // Windows always has PowerShell speech
14
+ voiceAvailable = true;
15
+ } else if (process.platform === 'darwin') {
16
+ // macOS - check for sox
17
+ await execa('which', ['sox'], { reject: true });
18
+ voiceAvailable = true;
19
+ } else {
20
+ // Linux - check for sox or arecord
21
+ try {
22
+ await execa('which', ['sox'], { reject: true });
23
+ voiceAvailable = true;
24
+ } catch {
25
+ await execa('which', ['arecord'], { reject: true });
26
+ voiceAvailable = true;
27
+ }
28
+ }
29
+ } catch {
30
+ voiceAvailable = false;
31
+ }
32
+
33
+ return voiceAvailable;
34
+ }
35
+
36
+ /**
37
+ * Enhanced text input that supports voice
38
+ * User can type "voice" or "v" to switch to voice input
39
+ */
40
+ export async function textWithVoice(options: {
41
+ message: string;
42
+ placeholder?: string;
43
+ initialValue?: string;
44
+ validate?: (value: string) => string | undefined;
45
+ }): Promise<string | symbol> {
46
+ const hasVoice = await checkVoiceAvailability();
47
+
48
+ const hint = hasVoice ? chalk.dim(' (type "v" for voice)') : '';
49
+
50
+ const result = await p.text({
51
+ message: options.message + hint,
52
+ placeholder: options.placeholder,
53
+ initialValue: options.initialValue,
54
+ validate: (value) => {
55
+ // Don't validate if they're switching to voice
56
+ if (value.toLowerCase() === 'v' || value.toLowerCase() === 'voice') {
57
+ return undefined;
58
+ }
59
+ return options.validate?.(value);
60
+ },
61
+ });
62
+
63
+ if (p.isCancel(result)) return result;
64
+
65
+ // Check if user wants voice input
66
+ const val = (result as string).toLowerCase().trim();
67
+ if (val === 'v' || val === 'voice') {
68
+ if (!hasVoice) {
69
+ console.log(chalk.yellow('Voice input not available on this system.'));
70
+ return textWithVoice(options); // Try again with text
71
+ }
72
+
73
+ const voiceResult = await getVoiceInput(options.message);
74
+ if (voiceResult === null) {
75
+ return textWithVoice(options); // Cancelled, try again
76
+ }
77
+
78
+ // Validate voice result
79
+ if (options.validate) {
80
+ const error = options.validate(voiceResult);
81
+ if (error) {
82
+ console.log(chalk.red(error));
83
+ return textWithVoice(options);
84
+ }
85
+ }
86
+
87
+ return voiceResult;
88
+ }
89
+
90
+ return result;
91
+ }
92
+
93
+ /**
94
+ * Get voice input from microphone
95
+ */
96
+ export async function getVoiceInput(prompt: string): Promise<string | null> {
97
+ console.log(chalk.cyan(`\n🎤 ${prompt}`));
98
+ console.log(chalk.dim(' Speak after the beep. Recording stops after silence.\n'));
99
+
100
+ const ready = await p.confirm({
101
+ message: 'Ready to record?',
102
+ initialValue: true,
103
+ });
104
+
105
+ if (!ready || p.isCancel(ready)) {
106
+ return null;
107
+ }
108
+
109
+ // Play a beep sound if possible
110
+ await playBeep();
111
+
112
+ const spinner = p.spinner();
113
+ spinner.start('🔴 Recording...');
114
+
115
+ try {
116
+ let transcription = '';
117
+
118
+ if (process.platform === 'win32') {
119
+ transcription = await recordWithWindowsSpeech();
120
+ } else if (process.platform === 'darwin') {
121
+ transcription = await recordWithMacOS();
122
+ } else {
123
+ transcription = await recordWithLinux();
124
+ }
125
+
126
+ spinner.stop('Recording complete');
127
+
128
+ if (transcription) {
129
+ console.log(chalk.green(`\n ✓ Heard: "${transcription}"\n`));
130
+
131
+ const confirm = await p.confirm({
132
+ message: 'Is this correct?',
133
+ initialValue: true,
134
+ });
135
+
136
+ if (confirm && !p.isCancel(confirm)) {
137
+ return transcription;
138
+ } else {
139
+ const action = await p.select({
140
+ message: 'What would you like to do?',
141
+ options: [
142
+ { value: 'retry', label: '🎤 Try again' },
143
+ { value: 'type', label: '⌨️ Type instead' },
144
+ { value: 'cancel', label: '✗ Cancel' },
145
+ ],
146
+ });
147
+
148
+ if (p.isCancel(action) || action === 'cancel') return null;
149
+
150
+ if (action === 'retry') {
151
+ return await getVoiceInput(prompt);
152
+ } else {
153
+ const text = await p.text({ message: 'Type your response:' });
154
+ return p.isCancel(text) ? null : text as string;
155
+ }
156
+ }
157
+ } else {
158
+ console.log(chalk.yellow('\n No speech detected. Try again or type your response.\n'));
159
+
160
+ const text = await p.text({ message: 'Type instead:' });
161
+ return p.isCancel(text) ? null : text as string;
162
+ }
163
+ } catch (error) {
164
+ spinner.stop('Recording failed');
165
+ console.log(chalk.yellow('Voice input failed. Please type instead.'));
166
+
167
+ const text = await p.text({ message: prompt });
168
+ return p.isCancel(text) ? null : text as string;
169
+ }
170
+ }
171
+
172
+ async function playBeep(): Promise<void> {
173
+ try {
174
+ if (process.platform === 'win32') {
175
+ await execa('powershell', ['-Command', '[console]::beep(800,200)'], { reject: false });
176
+ } else if (process.platform === 'darwin') {
177
+ await execa('afplay', ['/System/Library/Sounds/Tink.aiff'], { reject: false });
178
+ } else {
179
+ // Linux - try beep or paplay
180
+ try {
181
+ await execa('beep', ['-f', '800', '-l', '200'], { reject: false });
182
+ } catch {
183
+ await execa('paplay', ['/usr/share/sounds/freedesktop/stereo/message.oga'], { reject: false });
184
+ }
185
+ }
186
+ } catch {
187
+ // Silent fail - beep is optional
188
+ }
189
+ }
190
+
191
+ async function recordWithWindowsSpeech(): Promise<string> {
192
+ const psScript = `
193
+ Add-Type -AssemblyName System.Speech
194
+ $recognizer = New-Object System.Speech.Recognition.SpeechRecognitionEngine
195
+ $recognizer.SetInputToDefaultAudioDevice()
196
+ $grammar = New-Object System.Speech.Recognition.DictationGrammar
197
+ $recognizer.LoadGrammar($grammar)
198
+ try {
199
+ $result = $recognizer.Recognize([TimeSpan]::FromSeconds(10))
200
+ if ($result) {
201
+ Write-Output $result.Text
202
+ }
203
+ } finally {
204
+ $recognizer.Dispose()
205
+ }
206
+ `;
207
+
208
+ try {
209
+ const result = await execa('powershell', ['-Command', psScript], {
210
+ timeout: 15000,
211
+ });
212
+ return result.stdout.trim();
213
+ } catch {
214
+ return '';
215
+ }
216
+ }
217
+
218
+ async function recordWithMacOS(): Promise<string> {
219
+ const tempFile = `/tmp/codebakers-voice-${Date.now()}.wav`;
220
+
221
+ try {
222
+ // Record with sox - stops on silence
223
+ await execa('sox', [
224
+ '-d', // default input
225
+ '-r', '16000', // sample rate
226
+ '-c', '1', // mono
227
+ '-b', '16', // 16-bit
228
+ tempFile,
229
+ 'silence', '1', '0.1', '3%', // start on sound
230
+ '1', '2.0', '3%', // stop after 2s silence
231
+ 'trim', '0', '30', // max 30 seconds
232
+ ], { timeout: 35000 });
233
+
234
+ // Try whisper for transcription
235
+ return await transcribeWithWhisper(tempFile);
236
+ } finally {
237
+ await fs.remove(tempFile).catch(() => {});
238
+ }
239
+ }
240
+
241
+ async function recordWithLinux(): Promise<string> {
242
+ const tempFile = `/tmp/codebakers-voice-${Date.now()}.wav`;
243
+
244
+ try {
245
+ // Try sox first
246
+ try {
247
+ await execa('sox', [
248
+ '-d', tempFile,
249
+ 'rate', '16000',
250
+ 'channels', '1',
251
+ 'silence', '1', '0.1', '3%',
252
+ '1', '2.0', '3%',
253
+ 'trim', '0', '30',
254
+ ], { timeout: 35000 });
255
+ } catch {
256
+ // Fall back to arecord
257
+ await execa('arecord', [
258
+ '-f', 'S16_LE',
259
+ '-r', '16000',
260
+ '-c', '1',
261
+ '-d', '10',
262
+ tempFile,
263
+ ], { timeout: 15000 });
264
+ }
265
+
266
+ return await transcribeWithWhisper(tempFile);
267
+ } finally {
268
+ await fs.remove(tempFile).catch(() => {});
269
+ }
270
+ }
271
+
272
+ async function transcribeWithWhisper(audioFile: string): Promise<string> {
273
+ try {
274
+ // Try local whisper first
275
+ const outputBase = audioFile.replace('.wav', '');
276
+
277
+ await execa('whisper', [
278
+ audioFile,
279
+ '--language', 'en',
280
+ '--output_format', 'txt',
281
+ '--output_dir', '/tmp',
282
+ ], { timeout: 60000 });
283
+
284
+ const txtFile = outputBase + '.txt';
285
+ if (await fs.pathExists(txtFile)) {
286
+ const text = await fs.readFile(txtFile, 'utf-8');
287
+ await fs.remove(txtFile).catch(() => {});
288
+ return text.trim();
289
+ }
290
+ } catch {
291
+ // Whisper not available - try online API or return empty
292
+ }
293
+
294
+ return '';
295
+ }
296
+
297
+ /**
298
+ * Voice-enabled select (read options aloud, accept voice selection)
299
+ */
300
+ export async function selectWithVoice<T extends string>(options: {
301
+ message: string;
302
+ options: Array<{ value: T; label: string; hint?: string }>;
303
+ }): Promise<T | symbol> {
304
+ // For now, just use regular select
305
+ // Voice selection is complex and error-prone
306
+ return p.select(options) as Promise<T | symbol>;
307
+ }
308
+
309
+ /**
310
+ * Voice-enabled confirm
311
+ */
312
+ export async function confirmWithVoice(options: {
313
+ message: string;
314
+ initialValue?: boolean;
315
+ }): Promise<boolean | symbol> {
316
+ const hasVoice = await checkVoiceAvailability();
317
+ const hint = hasVoice ? chalk.dim(' (say "yes" or "no")') : '';
318
+
319
+ return p.confirm({
320
+ message: options.message + hint,
321
+ initialValue: options.initialValue,
322
+ });
323
+ }