codebakers 2.0.3 → 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.
- package/dist/advisors-J3S64IZK.js +7 -0
- package/dist/chunk-FWQNLNTI.js +565 -0
- package/dist/chunk-RCC7FYEU.js +319 -0
- package/dist/chunk-YGVDLNXY.js +326 -0
- package/dist/index.js +2813 -1559
- package/dist/prd-HBUCYLVG.js +7 -0
- package/package.json +1 -1
- package/src/commands/build.ts +989 -0
- package/src/commands/code.ts +102 -5
- package/src/commands/migrate.ts +419 -0
- package/src/commands/prd-maker.ts +587 -0
- package/src/index.ts +136 -4
- package/src/utils/files.ts +418 -0
- package/src/utils/nlp.ts +312 -0
- package/src/utils/voice.ts +323 -0
|
@@ -0,0 +1,587 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import * as fs from 'fs-extra';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
6
|
+
import { Config } from '../utils/config.js';
|
|
7
|
+
import { textWithVoice, getVoiceInput, checkVoiceAvailability } from '../utils/voice.js';
|
|
8
|
+
|
|
9
|
+
interface PRDInput {
|
|
10
|
+
projectName: string;
|
|
11
|
+
problemStatement: string;
|
|
12
|
+
targetUsers: string;
|
|
13
|
+
coreFeatures: string[];
|
|
14
|
+
userStories: string[];
|
|
15
|
+
technicalRequirements: string;
|
|
16
|
+
designPreferences: string;
|
|
17
|
+
timeline: string;
|
|
18
|
+
budget: string;
|
|
19
|
+
competitors: string;
|
|
20
|
+
successMetrics: string;
|
|
21
|
+
additionalNotes: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function prdMakerCommand(): Promise<void> {
|
|
25
|
+
const config = new Config();
|
|
26
|
+
|
|
27
|
+
if (!config.isConfigured()) {
|
|
28
|
+
p.log.error('Please run `codebakers setup` first.');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const anthropicCreds = config.getCredentials('anthropic');
|
|
33
|
+
if (!anthropicCreds?.apiKey) {
|
|
34
|
+
p.log.error('Anthropic API key not configured.');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(chalk.cyan(`
|
|
39
|
+
╭─────────────────────────────────────────────────────────────────╮
|
|
40
|
+
│ │
|
|
41
|
+
│ 📝 CODEBAKERS PRD MAKER │
|
|
42
|
+
│ │
|
|
43
|
+
│ I'll interview you about your product idea and generate │
|
|
44
|
+
│ a professional PRD document you can use to: │
|
|
45
|
+
│ │
|
|
46
|
+
│ • Share with developers or stakeholders │
|
|
47
|
+
│ • Feed directly into \`codebakers prd\` to build it │
|
|
48
|
+
│ • Get Dream Team feedback with \`codebakers advisors\` │
|
|
49
|
+
│ │
|
|
50
|
+
│ 🎤 TIP: You can use voice input! Say "voice" at any prompt. │
|
|
51
|
+
│ │
|
|
52
|
+
╰─────────────────────────────────────────────────────────────────╯
|
|
53
|
+
`));
|
|
54
|
+
|
|
55
|
+
const inputMethod = await p.select({
|
|
56
|
+
message: 'How do you want to provide input?',
|
|
57
|
+
options: [
|
|
58
|
+
{ value: 'text', label: '⌨️ Type responses' },
|
|
59
|
+
{ value: 'voice', label: '🎤 Use microphone (voice input)' },
|
|
60
|
+
{ value: 'mixed', label: '🔀 Mixed (type or speak per question)' },
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (p.isCancel(inputMethod)) return;
|
|
65
|
+
|
|
66
|
+
const useVoice = inputMethod === 'voice' || inputMethod === 'mixed';
|
|
67
|
+
|
|
68
|
+
if (useVoice) {
|
|
69
|
+
const voiceAvailable = await checkVoiceAvailability();
|
|
70
|
+
if (!voiceAvailable) {
|
|
71
|
+
p.log.warn('Voice input not available on this system. Falling back to text.');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Conduct the interview
|
|
76
|
+
const input = await conductPRDInterview(inputMethod as string);
|
|
77
|
+
if (!input) return;
|
|
78
|
+
|
|
79
|
+
// Generate PRD with Claude
|
|
80
|
+
const anthropic = new Anthropic({ apiKey: anthropicCreds.apiKey });
|
|
81
|
+
|
|
82
|
+
const spinner = p.spinner();
|
|
83
|
+
spinner.start('Generating professional PRD...');
|
|
84
|
+
|
|
85
|
+
const prd = await generatePRD(anthropic, input);
|
|
86
|
+
|
|
87
|
+
spinner.stop('PRD generated!');
|
|
88
|
+
|
|
89
|
+
// Save PRD
|
|
90
|
+
const filename = `${input.projectName.toLowerCase().replace(/[^a-z0-9]/g, '-')}-prd.md`;
|
|
91
|
+
const filepath = path.join(process.cwd(), filename);
|
|
92
|
+
await fs.writeFile(filepath, prd);
|
|
93
|
+
|
|
94
|
+
// Also save as JSON for programmatic use
|
|
95
|
+
const jsonPath = filepath.replace('.md', '.json');
|
|
96
|
+
await fs.writeJson(jsonPath, input, { spaces: 2 });
|
|
97
|
+
|
|
98
|
+
console.log(chalk.green(`
|
|
99
|
+
╔════════════════════════════════════════════════════════════════╗
|
|
100
|
+
║ ✓ PRD Generated Successfully! ║
|
|
101
|
+
╠════════════════════════════════════════════════════════════════╣
|
|
102
|
+
║ ║
|
|
103
|
+
║ 📄 Markdown: ${filename.padEnd(43)}║
|
|
104
|
+
║ 📊 Data: ${(filename.replace('.md', '.json')).padEnd(43)}║
|
|
105
|
+
║ ║
|
|
106
|
+
╠════════════════════════════════════════════════════════════════╣
|
|
107
|
+
║ Next steps: ║
|
|
108
|
+
║ ║
|
|
109
|
+
║ • View PRD: cat ${filename.padEnd(34)}║
|
|
110
|
+
║ • Build it: codebakers prd ${filename.padEnd(23)}║
|
|
111
|
+
║ • Get feedback: codebakers advisors ║
|
|
112
|
+
║ ║
|
|
113
|
+
╚════════════════════════════════════════════════════════════════╝
|
|
114
|
+
`));
|
|
115
|
+
|
|
116
|
+
// Preview option
|
|
117
|
+
const preview = await p.confirm({
|
|
118
|
+
message: 'Preview the PRD now?',
|
|
119
|
+
initialValue: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (preview && !p.isCancel(preview)) {
|
|
123
|
+
console.log(chalk.cyan('\n' + '═'.repeat(60) + '\n'));
|
|
124
|
+
console.log(prd);
|
|
125
|
+
console.log(chalk.cyan('\n' + '═'.repeat(60) + '\n'));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Offer next steps
|
|
129
|
+
const nextStep = await p.select({
|
|
130
|
+
message: 'What would you like to do next?',
|
|
131
|
+
options: [
|
|
132
|
+
{ value: 'build', label: '🚀 Build this project now' },
|
|
133
|
+
{ value: 'advisors', label: '🌟 Get Dream Team feedback first' },
|
|
134
|
+
{ value: 'done', label: '✓ Done for now' },
|
|
135
|
+
],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (p.isCancel(nextStep) || nextStep === 'done') {
|
|
139
|
+
p.outro('PRD saved!');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (nextStep === 'build') {
|
|
144
|
+
const { prdCommand } = await import('./prd.js');
|
|
145
|
+
await prdCommand(filepath);
|
|
146
|
+
} else if (nextStep === 'advisors') {
|
|
147
|
+
const { advisorsCommand } = await import('./advisors.js');
|
|
148
|
+
await advisorsCommand();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function conductPRDInterview(inputMethod: string): Promise<PRDInput | null> {
|
|
153
|
+
const getInput = async (message: string, placeholder?: string): Promise<string | null> => {
|
|
154
|
+
if (inputMethod === 'voice') {
|
|
155
|
+
return await getVoiceInput(message);
|
|
156
|
+
} else if (inputMethod === 'mixed') {
|
|
157
|
+
const method = await p.select({
|
|
158
|
+
message: `${message}`,
|
|
159
|
+
options: [
|
|
160
|
+
{ value: 'type', label: '⌨️ Type response' },
|
|
161
|
+
{ value: 'voice', label: '🎤 Speak response' },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
if (p.isCancel(method)) return null;
|
|
165
|
+
|
|
166
|
+
if (method === 'voice') {
|
|
167
|
+
return await getVoiceInput(message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const response = await p.text({
|
|
172
|
+
message,
|
|
173
|
+
placeholder,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (p.isCancel(response)) return null;
|
|
177
|
+
return response as string;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
console.log(chalk.bold('\n📋 Let\'s build your PRD!\n'));
|
|
181
|
+
|
|
182
|
+
// Project basics
|
|
183
|
+
const projectName = await getInput(
|
|
184
|
+
'What\'s your project/product name?',
|
|
185
|
+
'My Awesome App'
|
|
186
|
+
);
|
|
187
|
+
if (!projectName) return null;
|
|
188
|
+
|
|
189
|
+
const problemStatement = await getInput(
|
|
190
|
+
'What problem does this solve? Who has this problem and why is it painful?',
|
|
191
|
+
'Small business owners struggle with...'
|
|
192
|
+
);
|
|
193
|
+
if (!problemStatement) return null;
|
|
194
|
+
|
|
195
|
+
const targetUsers = await getInput(
|
|
196
|
+
'Who are your target users? Be specific about demographics, roles, etc.',
|
|
197
|
+
'Marketing managers at companies with 10-50 employees...'
|
|
198
|
+
);
|
|
199
|
+
if (!targetUsers) return null;
|
|
200
|
+
|
|
201
|
+
// Features
|
|
202
|
+
console.log(chalk.bold('\n🎯 Core Features\n'));
|
|
203
|
+
console.log(chalk.dim('List the main features. Type each one and press Enter. Type "done" when finished.\n'));
|
|
204
|
+
|
|
205
|
+
const coreFeatures: string[] = [];
|
|
206
|
+
while (true) {
|
|
207
|
+
const feature = await getInput(
|
|
208
|
+
`Feature ${coreFeatures.length + 1} (or "done"):`,
|
|
209
|
+
'User authentication with social login'
|
|
210
|
+
);
|
|
211
|
+
if (!feature) return null;
|
|
212
|
+
if (feature.toLowerCase() === 'done') break;
|
|
213
|
+
coreFeatures.push(feature);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// User stories
|
|
217
|
+
console.log(chalk.bold('\n👤 User Stories\n'));
|
|
218
|
+
console.log(chalk.dim('Describe key user journeys. Type "done" when finished.\n'));
|
|
219
|
+
|
|
220
|
+
const userStories: string[] = [];
|
|
221
|
+
while (true) {
|
|
222
|
+
const story = await getInput(
|
|
223
|
+
`User story ${userStories.length + 1} (or "done"):`,
|
|
224
|
+
'As a user, I want to... so that I can...'
|
|
225
|
+
);
|
|
226
|
+
if (!story) return null;
|
|
227
|
+
if (story.toLowerCase() === 'done') break;
|
|
228
|
+
userStories.push(story);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Technical
|
|
232
|
+
const technicalRequirements = await getInput(
|
|
233
|
+
'Any technical requirements or constraints? (platforms, integrations, etc.)',
|
|
234
|
+
'Must work on mobile, integrate with Stripe, support 10k users...'
|
|
235
|
+
);
|
|
236
|
+
if (!technicalRequirements) return null;
|
|
237
|
+
|
|
238
|
+
// Design
|
|
239
|
+
const designPreferences = await getInput(
|
|
240
|
+
'Design preferences? (style, colors, inspiration sites)',
|
|
241
|
+
'Clean and minimal like Linear, blue accent color...'
|
|
242
|
+
);
|
|
243
|
+
if (!designPreferences) return null;
|
|
244
|
+
|
|
245
|
+
// Business
|
|
246
|
+
const timeline = await p.select({
|
|
247
|
+
message: 'What\'s your timeline?',
|
|
248
|
+
options: [
|
|
249
|
+
{ value: '1-week', label: '1 week (MVP)' },
|
|
250
|
+
{ value: '2-weeks', label: '2 weeks' },
|
|
251
|
+
{ value: '1-month', label: '1 month' },
|
|
252
|
+
{ value: '3-months', label: '3 months' },
|
|
253
|
+
{ value: 'flexible', label: 'Flexible' },
|
|
254
|
+
],
|
|
255
|
+
});
|
|
256
|
+
if (p.isCancel(timeline)) return null;
|
|
257
|
+
|
|
258
|
+
const budget = await p.select({
|
|
259
|
+
message: 'What\'s your budget?',
|
|
260
|
+
options: [
|
|
261
|
+
{ value: 'bootstrap', label: 'Bootstrap (free tiers only)' },
|
|
262
|
+
{ value: 'small', label: 'Small ($100-500/month)' },
|
|
263
|
+
{ value: 'medium', label: 'Medium ($500-2000/month)' },
|
|
264
|
+
{ value: 'funded', label: 'Funded ($2000+/month)' },
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
if (p.isCancel(budget)) return null;
|
|
268
|
+
|
|
269
|
+
const competitors = await getInput(
|
|
270
|
+
'Who are your competitors? How are you different?',
|
|
271
|
+
'Competitor X does Y, but we focus on Z...'
|
|
272
|
+
);
|
|
273
|
+
if (!competitors) return null;
|
|
274
|
+
|
|
275
|
+
const successMetrics = await getInput(
|
|
276
|
+
'How will you measure success? What are your KPIs?',
|
|
277
|
+
'1000 users in first month, 5% conversion rate...'
|
|
278
|
+
);
|
|
279
|
+
if (!successMetrics) return null;
|
|
280
|
+
|
|
281
|
+
const additionalNotes = await getInput(
|
|
282
|
+
'Anything else we should know?',
|
|
283
|
+
'Optional: special requirements, existing assets, etc.'
|
|
284
|
+
);
|
|
285
|
+
if (additionalNotes === null) return null;
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
projectName,
|
|
289
|
+
problemStatement,
|
|
290
|
+
targetUsers,
|
|
291
|
+
coreFeatures,
|
|
292
|
+
userStories,
|
|
293
|
+
technicalRequirements,
|
|
294
|
+
designPreferences,
|
|
295
|
+
timeline: timeline as string,
|
|
296
|
+
budget: budget as string,
|
|
297
|
+
competitors,
|
|
298
|
+
successMetrics,
|
|
299
|
+
additionalNotes: additionalNotes || '',
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function checkVoiceAvailability(): Promise<boolean> {
|
|
304
|
+
try {
|
|
305
|
+
// Check for sox (Sound eXchange) - cross-platform audio tool
|
|
306
|
+
await execa('sox', ['--version'], { reject: true });
|
|
307
|
+
return true;
|
|
308
|
+
} catch {
|
|
309
|
+
try {
|
|
310
|
+
// Windows: check for powershell speech recognition
|
|
311
|
+
if (process.platform === 'win32') {
|
|
312
|
+
return true; // We'll use PowerShell
|
|
313
|
+
}
|
|
314
|
+
// macOS: check for say command (for feedback)
|
|
315
|
+
if (process.platform === 'darwin') {
|
|
316
|
+
await execa('which', ['say'], { reject: true });
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function getVoiceInput(prompt: string): Promise<string | null> {
|
|
327
|
+
console.log(chalk.cyan(`\n🎤 ${prompt}`));
|
|
328
|
+
console.log(chalk.dim(' Press Enter to start recording, then speak. Press Enter again to stop.\n'));
|
|
329
|
+
|
|
330
|
+
const ready = await p.confirm({
|
|
331
|
+
message: 'Ready to record?',
|
|
332
|
+
initialValue: true,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
if (!ready || p.isCancel(ready)) {
|
|
336
|
+
// Fall back to text
|
|
337
|
+
const text = await p.text({ message: 'Type instead:' });
|
|
338
|
+
return p.isCancel(text) ? null : text as string;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const spinner = p.spinner();
|
|
342
|
+
spinner.start('🔴 Recording... (press Ctrl+C to stop)');
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
let transcription = '';
|
|
346
|
+
|
|
347
|
+
if (process.platform === 'win32') {
|
|
348
|
+
// Windows: Use PowerShell with System.Speech
|
|
349
|
+
transcription = await recordWithWindowsSpeech();
|
|
350
|
+
} else if (process.platform === 'darwin') {
|
|
351
|
+
// macOS: Use sox + whisper or built-in dictation
|
|
352
|
+
transcription = await recordWithMacOS();
|
|
353
|
+
} else {
|
|
354
|
+
// Linux: Use sox + whisper
|
|
355
|
+
transcription = await recordWithLinux();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
spinner.stop('Recording complete');
|
|
359
|
+
|
|
360
|
+
if (transcription) {
|
|
361
|
+
console.log(chalk.green(`\n Heard: "${transcription}"\n`));
|
|
362
|
+
|
|
363
|
+
const confirm = await p.confirm({
|
|
364
|
+
message: 'Is this correct?',
|
|
365
|
+
initialValue: true,
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
if (confirm && !p.isCancel(confirm)) {
|
|
369
|
+
return transcription;
|
|
370
|
+
} else {
|
|
371
|
+
// Let them try again or type
|
|
372
|
+
const retry = await p.select({
|
|
373
|
+
message: 'What would you like to do?',
|
|
374
|
+
options: [
|
|
375
|
+
{ value: 'retry', label: '🎤 Try again' },
|
|
376
|
+
{ value: 'type', label: '⌨️ Type instead' },
|
|
377
|
+
],
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (p.isCancel(retry)) return null;
|
|
381
|
+
|
|
382
|
+
if (retry === 'retry') {
|
|
383
|
+
return await getVoiceInput(prompt);
|
|
384
|
+
} else {
|
|
385
|
+
const text = await p.text({ message: 'Type your response:' });
|
|
386
|
+
return p.isCancel(text) ? null : text as string;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
spinner.stop('Recording failed');
|
|
392
|
+
console.log(chalk.yellow('Voice input failed. Falling back to text.'));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Fallback to text
|
|
396
|
+
const text = await p.text({ message: prompt });
|
|
397
|
+
return p.isCancel(text) ? null : text as string;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function recordWithWindowsSpeech(): Promise<string> {
|
|
401
|
+
// PowerShell script for Windows Speech Recognition
|
|
402
|
+
const psScript = `
|
|
403
|
+
Add-Type -AssemblyName System.Speech
|
|
404
|
+
$recognizer = New-Object System.Speech.Recognition.SpeechRecognitionEngine
|
|
405
|
+
$recognizer.SetInputToDefaultAudioDevice()
|
|
406
|
+
$grammar = New-Object System.Speech.Recognition.DictationGrammar
|
|
407
|
+
$recognizer.LoadGrammar($grammar)
|
|
408
|
+
$result = $recognizer.Recognize()
|
|
409
|
+
if ($result) { Write-Output $result.Text }
|
|
410
|
+
`;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const result = await execa('powershell', ['-Command', psScript], {
|
|
414
|
+
timeout: 30000, // 30 second timeout
|
|
415
|
+
});
|
|
416
|
+
return result.stdout.trim();
|
|
417
|
+
} catch {
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function recordWithMacOS(): Promise<string> {
|
|
423
|
+
// Use sox to record, then use macOS dictation or whisper
|
|
424
|
+
const tempFile = `/tmp/codebakers-voice-${Date.now()}.wav`;
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
// Record 10 seconds of audio
|
|
428
|
+
console.log(chalk.dim(' Recording for up to 10 seconds...'));
|
|
429
|
+
|
|
430
|
+
await execa('sox', [
|
|
431
|
+
'-d', // default input device
|
|
432
|
+
'-r', '16000', // sample rate
|
|
433
|
+
'-c', '1', // mono
|
|
434
|
+
'-b', '16', // 16-bit
|
|
435
|
+
tempFile,
|
|
436
|
+
'trim', '0', '10', // max 10 seconds
|
|
437
|
+
'silence', '1', '0.5', '1%', // stop on silence
|
|
438
|
+
], { timeout: 15000 });
|
|
439
|
+
|
|
440
|
+
// Try to use whisper if available, otherwise return empty
|
|
441
|
+
try {
|
|
442
|
+
const result = await execa('whisper', [tempFile, '--language', 'en', '--output_format', 'txt'], {
|
|
443
|
+
timeout: 30000,
|
|
444
|
+
});
|
|
445
|
+
const txtFile = tempFile.replace('.wav', '.txt');
|
|
446
|
+
if (await fs.pathExists(txtFile)) {
|
|
447
|
+
const text = await fs.readFile(txtFile, 'utf-8');
|
|
448
|
+
await fs.remove(txtFile);
|
|
449
|
+
return text.trim();
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
// Whisper not available
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return '';
|
|
456
|
+
} finally {
|
|
457
|
+
// Cleanup
|
|
458
|
+
await fs.remove(tempFile).catch(() => {});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function recordWithLinux(): Promise<string> {
|
|
463
|
+
// Similar to macOS but with arecord as fallback
|
|
464
|
+
const tempFile = `/tmp/codebakers-voice-${Date.now()}.wav`;
|
|
465
|
+
|
|
466
|
+
try {
|
|
467
|
+
// Try sox first
|
|
468
|
+
try {
|
|
469
|
+
await execa('sox', [
|
|
470
|
+
'-d', tempFile,
|
|
471
|
+
'rate', '16000',
|
|
472
|
+
'channels', '1',
|
|
473
|
+
'trim', '0', '10',
|
|
474
|
+
'silence', '1', '0.5', '1%',
|
|
475
|
+
], { timeout: 15000 });
|
|
476
|
+
} catch {
|
|
477
|
+
// Fall back to arecord
|
|
478
|
+
await execa('arecord', [
|
|
479
|
+
'-f', 'S16_LE',
|
|
480
|
+
'-r', '16000',
|
|
481
|
+
'-c', '1',
|
|
482
|
+
'-d', '10',
|
|
483
|
+
tempFile,
|
|
484
|
+
], { timeout: 15000 });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Try whisper
|
|
488
|
+
try {
|
|
489
|
+
const result = await execa('whisper', [tempFile, '--language', 'en', '--output_format', 'txt'], {
|
|
490
|
+
timeout: 30000,
|
|
491
|
+
});
|
|
492
|
+
const txtFile = tempFile.replace('.wav', '.txt');
|
|
493
|
+
if (await fs.pathExists(txtFile)) {
|
|
494
|
+
const text = await fs.readFile(txtFile, 'utf-8');
|
|
495
|
+
await fs.remove(txtFile);
|
|
496
|
+
return text.trim();
|
|
497
|
+
}
|
|
498
|
+
} catch {
|
|
499
|
+
// Whisper not available
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return '';
|
|
503
|
+
} finally {
|
|
504
|
+
await fs.remove(tempFile).catch(() => {});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
async function generatePRD(anthropic: Anthropic, input: PRDInput): Promise<string> {
|
|
509
|
+
const response = await anthropic.messages.create({
|
|
510
|
+
model: 'claude-sonnet-4-20250514',
|
|
511
|
+
max_tokens: 8192,
|
|
512
|
+
messages: [{
|
|
513
|
+
role: 'user',
|
|
514
|
+
content: `Generate a professional, comprehensive Product Requirements Document (PRD) based on this input:
|
|
515
|
+
|
|
516
|
+
PROJECT NAME: ${input.projectName}
|
|
517
|
+
|
|
518
|
+
PROBLEM STATEMENT:
|
|
519
|
+
${input.problemStatement}
|
|
520
|
+
|
|
521
|
+
TARGET USERS:
|
|
522
|
+
${input.targetUsers}
|
|
523
|
+
|
|
524
|
+
CORE FEATURES:
|
|
525
|
+
${input.coreFeatures.map((f, i) => `${i + 1}. ${f}`).join('\n')}
|
|
526
|
+
|
|
527
|
+
USER STORIES:
|
|
528
|
+
${input.userStories.map((s, i) => `${i + 1}. ${s}`).join('\n')}
|
|
529
|
+
|
|
530
|
+
TECHNICAL REQUIREMENTS:
|
|
531
|
+
${input.technicalRequirements}
|
|
532
|
+
|
|
533
|
+
DESIGN PREFERENCES:
|
|
534
|
+
${input.designPreferences}
|
|
535
|
+
|
|
536
|
+
TIMELINE: ${input.timeline}
|
|
537
|
+
BUDGET: ${input.budget}
|
|
538
|
+
|
|
539
|
+
COMPETITORS:
|
|
540
|
+
${input.competitors}
|
|
541
|
+
|
|
542
|
+
SUCCESS METRICS:
|
|
543
|
+
${input.successMetrics}
|
|
544
|
+
|
|
545
|
+
ADDITIONAL NOTES:
|
|
546
|
+
${input.additionalNotes}
|
|
547
|
+
|
|
548
|
+
---
|
|
549
|
+
|
|
550
|
+
Generate a detailed PRD in Markdown format with the following sections:
|
|
551
|
+
|
|
552
|
+
1. **Executive Summary** - Brief overview of the product
|
|
553
|
+
2. **Problem Statement** - Detailed problem analysis
|
|
554
|
+
3. **Target Audience** - User personas with demographics
|
|
555
|
+
4. **Goals & Objectives** - SMART goals
|
|
556
|
+
5. **Features & Requirements**
|
|
557
|
+
- Core Features (P0)
|
|
558
|
+
- Secondary Features (P1)
|
|
559
|
+
- Nice-to-Have (P2)
|
|
560
|
+
6. **User Stories & Flows** - Detailed user journeys
|
|
561
|
+
7. **Technical Specifications**
|
|
562
|
+
- Recommended Tech Stack
|
|
563
|
+
- Architecture Overview
|
|
564
|
+
- Database Schema (key tables)
|
|
565
|
+
- API Endpoints (key endpoints)
|
|
566
|
+
- Third-party Integrations
|
|
567
|
+
8. **Design Requirements**
|
|
568
|
+
- Design Principles
|
|
569
|
+
- Key Screens
|
|
570
|
+
- Branding Guidelines
|
|
571
|
+
9. **Timeline & Milestones**
|
|
572
|
+
- Phase breakdown
|
|
573
|
+
- Deliverables per phase
|
|
574
|
+
10. **Success Metrics & KPIs**
|
|
575
|
+
11. **Risks & Mitigations**
|
|
576
|
+
12. **Out of Scope** - What this does NOT include
|
|
577
|
+
13. **Open Questions** - Things that need clarification
|
|
578
|
+
14. **Appendix**
|
|
579
|
+
- Competitive Analysis
|
|
580
|
+
- Technical Glossary
|
|
581
|
+
|
|
582
|
+
Make it professional, detailed, and actionable. This should be ready to hand off to developers.`
|
|
583
|
+
}],
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
return response.content[0].type === 'text' ? response.content[0].text : '';
|
|
587
|
+
}
|