claude-autopm 2.2.2 → 2.4.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.
@@ -13,11 +13,13 @@
13
13
  */
14
14
 
15
15
  const PRDService = require('../../services/PRDService');
16
+ const ClaudeProvider = require('../../ai-providers/ClaudeProvider');
16
17
  const fs = require('fs-extra');
17
18
  const ora = require('ora');
18
19
  const chalk = require('chalk');
19
20
  const path = require('path');
20
21
  const { spawn } = require('child_process');
22
+ const readline = require('readline');
21
23
 
22
24
  /**
23
25
  * Get PRD file path
@@ -304,6 +306,12 @@ async function prdStatus(argv) {
304
306
  * @param {Object} argv - Command arguments
305
307
  */
306
308
  async function prdNew(argv) {
309
+ // Check if AI mode is enabled
310
+ if (argv.ai) {
311
+ return await prdNewWithAI(argv);
312
+ }
313
+
314
+ // Standard mode: spawn prd-new.js
307
315
  const spinner = ora(`Creating PRD: ${argv.name}`).start();
308
316
 
309
317
  try {
@@ -397,6 +405,171 @@ async function prdNew(argv) {
397
405
  }
398
406
  }
399
407
 
408
+ /**
409
+ * Create new PRD with AI assistance
410
+ * @param {Object} argv - Command arguments
411
+ */
412
+ async function prdNewWithAI(argv) {
413
+ console.log(chalk.cyan(`\nšŸ¤– AI-Powered PRD Creation: ${argv.name}`));
414
+ console.log(chalk.cyan('══════════════════════════════════════\n'));
415
+
416
+ const spinner = ora('Initializing...').start();
417
+
418
+ try {
419
+ // Check if PRD already exists
420
+ const prdPath = getPrdPath(argv.name);
421
+ const exists = await fs.pathExists(prdPath);
422
+ if (exists) {
423
+ spinner.fail(chalk.red('PRD already exists'));
424
+ console.error(chalk.red(`\nError: PRD file already exists: ${prdPath}`));
425
+ console.error(chalk.yellow('Use: autopm prd edit ' + argv.name + ' to modify it'));
426
+ process.exit(1);
427
+ }
428
+
429
+ // Create PRDs directory if needed
430
+ const prdsDir = path.join(process.cwd(), '.claude', 'prds');
431
+ await fs.ensureDir(prdsDir);
432
+
433
+ spinner.stop();
434
+
435
+ // Gather information from user
436
+ const rl = readline.createInterface({
437
+ input: process.stdin,
438
+ output: process.stdout
439
+ });
440
+
441
+ const prompt = (question) => new Promise((resolve) => {
442
+ rl.question(question, resolve);
443
+ });
444
+
445
+ console.log(chalk.cyan('🧠 Let\'s gather some information for AI...\n'));
446
+
447
+ const context = {};
448
+
449
+ // Product Vision
450
+ console.log(chalk.bold('šŸ“Œ Product Vision'));
451
+ console.log(chalk.gray('What problem are you solving? What\'s the vision?'));
452
+ context.vision = await prompt(chalk.cyan('Vision: '));
453
+
454
+ // Target Users
455
+ console.log(chalk.bold('\nšŸ‘„ Target Users'));
456
+ console.log(chalk.gray('Who will use this? What are their needs?'));
457
+ context.users = await prompt(chalk.cyan('Target users: '));
458
+
459
+ // Key Features (simplified - just high level)
460
+ console.log(chalk.bold('\n✨ Key Features'));
461
+ console.log(chalk.gray('What are the main capabilities? (brief description)'));
462
+ context.features = await prompt(chalk.cyan('Features: '));
463
+
464
+ // Success Metrics
465
+ console.log(chalk.bold('\nšŸ“Š Success Metrics'));
466
+ console.log(chalk.gray('How will you measure success?'));
467
+ context.metrics = await prompt(chalk.cyan('Metrics: '));
468
+
469
+ // Priority
470
+ console.log(chalk.bold('\nšŸŽÆ Priority'));
471
+ const priority = await prompt(chalk.cyan('Priority (P0/P1/P2/P3) [P2]: '));
472
+ context.priority = priority || 'P2';
473
+
474
+ // Timeline
475
+ console.log(chalk.bold('\nā° Timeline'));
476
+ const timeline = await prompt(chalk.cyan('Timeline [Q1 2025]: '));
477
+ context.timeline = timeline || 'Q1 2025';
478
+
479
+ rl.close();
480
+
481
+ // Build AI prompt
482
+ const aiPrompt = `Generate a comprehensive Product Requirements Document (PRD) based on the following information:
483
+
484
+ **PRD Name**: ${argv.name}
485
+ **Priority**: ${context.priority}
486
+ **Timeline**: ${context.timeline}
487
+
488
+ **Product Vision**:
489
+ ${context.vision}
490
+
491
+ **Target Users**:
492
+ ${context.users}
493
+
494
+ **Key Features**:
495
+ ${context.features}
496
+
497
+ **Success Metrics**:
498
+ ${context.metrics}
499
+
500
+ Please generate a complete, professional PRD with the following sections:
501
+ 1. Executive Summary
502
+ 2. Problem Statement (with Background, Current State, Desired State)
503
+ 3. Target Users (with User Personas and User Stories)
504
+ 4. Key Features (organized by priority: Must Have, Should Have, Nice to Have)
505
+ 5. Success Metrics (with KPIs and Measurement Plan)
506
+ 6. Technical Requirements (Architecture, Non-Functional Requirements, Dependencies)
507
+ 7. Implementation Plan (broken into phases)
508
+ 8. Risks and Mitigation
509
+ 9. Open Questions
510
+ 10. Appendix (References, Glossary, Changelog)
511
+
512
+ Format the output as a proper markdown document with frontmatter (status, priority, created, author, timeline).
513
+
514
+ Make it comprehensive, actionable, and professional. Expand on the provided information with industry best practices.`;
515
+
516
+ // Initialize AI provider and PRD service
517
+ const provider = new ClaudeProvider();
518
+ const prdService = new PRDService({ provider });
519
+ let prdContent = '';
520
+
521
+ if (argv.stream) {
522
+ // Streaming mode
523
+ const genSpinner = ora('Generating PRD with AI...').start();
524
+ genSpinner.stop();
525
+
526
+ console.log(chalk.cyan('\n\nšŸ¤– AI is generating your PRD...\n'));
527
+ console.log(chalk.gray('─'.repeat(80)) + '\n');
528
+
529
+ for await (const chunk of prdService.generatePRDStream(aiPrompt)) {
530
+ process.stdout.write(chunk);
531
+ prdContent += chunk;
532
+ }
533
+
534
+ console.log('\n\n' + chalk.gray('─'.repeat(80)));
535
+ } else {
536
+ // Non-streaming mode
537
+ const genSpinner = ora('Generating PRD with AI...').start();
538
+ prdContent = await prdService.generatePRD(aiPrompt);
539
+ genSpinner.succeed(chalk.green('PRD generated'));
540
+ }
541
+
542
+ // Save to file
543
+ const saveSpinner = ora('Saving PRD...').start();
544
+ await fs.writeFile(prdPath, prdContent);
545
+ saveSpinner.succeed(chalk.green('PRD saved'));
546
+
547
+ console.log(chalk.green('\nāœ… AI-powered PRD created successfully!'));
548
+ console.log(chalk.cyan(`šŸ“„ File: ${prdPath}\n`));
549
+
550
+ // Show next steps
551
+ console.log(chalk.bold('šŸ“‹ What You Can Do Next:\n'));
552
+ console.log(` ${chalk.cyan('1.')} Review and edit: ${chalk.yellow('autopm prd edit ' + argv.name)}`);
553
+ console.log(` ${chalk.cyan('2.')} Check status: ${chalk.yellow('autopm prd status ' + argv.name)}`);
554
+ console.log(` ${chalk.cyan('3.')} Parse to epic: ${chalk.yellow('autopm prd parse ' + argv.name + ' --ai')}`);
555
+ console.log(` ${chalk.cyan('4.')} Extract epics: ${chalk.yellow('autopm prd extract-epics ' + argv.name)}\n`);
556
+
557
+ } catch (error) {
558
+ console.error(chalk.red(`\nāœ— Error: ${error.message}`));
559
+
560
+ if (error.message.includes('ANTHROPIC_API_KEY') || error.message.includes('API key')) {
561
+ console.error(chalk.red('\nāœ— Error: API key not configured'));
562
+ console.error(chalk.yellow('\nšŸ’” Set your API key in .env file:'));
563
+ console.error(chalk.cyan(' ANTHROPIC_API_KEY=sk-ant-api03-...'));
564
+ } else if (process.env.DEBUG) {
565
+ console.error(chalk.gray('\nStack trace:'));
566
+ console.error(chalk.gray(error.stack));
567
+ }
568
+
569
+ process.exit(1);
570
+ }
571
+ }
572
+
400
573
  /**
401
574
  * Parse PRD with AI
402
575
  * @param {Object} argv - Command arguments
@@ -642,8 +815,20 @@ function builder(yargs) {
642
815
  type: 'string',
643
816
  alias: 't'
644
817
  })
818
+ .option('ai', {
819
+ describe: 'Use AI to generate PRD content (requires ANTHROPIC_API_KEY)',
820
+ type: 'boolean',
821
+ default: false
822
+ })
823
+ .option('stream', {
824
+ describe: 'Stream AI output in real-time (only with --ai)',
825
+ type: 'boolean',
826
+ default: false
827
+ })
645
828
  .example('autopm prd new my-feature', 'Create PRD with wizard')
646
- .example('autopm prd new payment-api --template api-feature', 'Create PRD from template');
829
+ .example('autopm prd new payment-api --template api-feature', 'Create PRD from template')
830
+ .example('autopm prd new my-feature --ai', 'AI-powered PRD generation')
831
+ .example('autopm prd new my-feature --ai --stream', 'AI generation with streaming');
647
832
  },
648
833
  prdNew // Handler
649
834
  )
@@ -40,26 +40,28 @@ class EpicService {
40
40
  * Create a new EpicService instance
41
41
  *
42
42
  * @param {Object} options - Configuration options
43
- * @param {PRDService} options.prdService - PRDService instance for parsing
43
+ * @param {PRDService} options.prdService - PRDService instance for parsing (optional for CLI operations)
44
44
  * @param {ConfigManager} [options.configManager] - Optional ConfigManager instance
45
45
  * @param {Object} [options.provider] - Optional AI provider instance for streaming
46
+ * @param {string} [options.epicsDir] - Path to epics directory (default: .claude/epics)
47
+ * @param {string} [options.defaultStatus] - Default epic status (default: backlog)
46
48
  */
47
49
  constructor(options = {}) {
48
- if (!options.prdService) {
49
- throw new Error('PRDService instance is required');
50
- }
51
-
52
- if (!(options.prdService instanceof PRDService)) {
53
- throw new Error('prdService must be an instance of PRDService');
54
- }
55
-
56
- this.prdService = options.prdService;
50
+ // PRDService is optional now - only required for PRD parsing operations
51
+ this.prdService = options.prdService || null;
57
52
 
58
53
  // Store ConfigManager if provided (for future use)
59
54
  this.configManager = options.configManager || undefined;
60
55
 
61
56
  // Store provider if provided (for streaming operations)
62
57
  this.provider = options.provider || undefined;
58
+
59
+ // CLI operation options
60
+ this.options = {
61
+ epicsDir: options.epicsDir || '.claude/epics',
62
+ defaultStatus: options.defaultStatus || 'backlog',
63
+ ...options
64
+ };
63
65
  }
64
66
 
65
67
  // ==========================================
@@ -312,6 +314,10 @@ class EpicService {
312
314
  * // }
313
315
  */
314
316
  analyzePRD(prdContent) {
317
+ if (!this.prdService) {
318
+ throw new Error('PRDService instance is required for PRD analysis operations');
319
+ }
320
+
315
321
  const frontmatter = this.prdService.parseFrontmatter(prdContent);
316
322
  const sections = this.prdService.extractPrdContent(prdContent);
317
323
 
@@ -321,6 +327,59 @@ class EpicService {
321
327
  };
322
328
  }
323
329
 
330
+ /**
331
+ * Parse YAML frontmatter from markdown content
332
+ *
333
+ * Extracts key-value pairs from YAML frontmatter block.
334
+ * Returns null if frontmatter is missing or malformed.
335
+ *
336
+ * @param {string} content - Markdown content with frontmatter
337
+ * @returns {Object|null} Parsed frontmatter object or null
338
+ */
339
+ parseFrontmatter(content) {
340
+ if (!content || typeof content !== 'string') {
341
+ return null;
342
+ }
343
+
344
+ // Match frontmatter block: ---\n...\n--- or ---\n---
345
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/) ||
346
+ content.match(/^---\n---/);
347
+
348
+ if (!frontmatterMatch) {
349
+ return null;
350
+ }
351
+
352
+ // Empty frontmatter (---\n---)
353
+ if (!frontmatterMatch[1] && content.startsWith('---\n---')) {
354
+ return {};
355
+ }
356
+
357
+ const frontmatter = {};
358
+ const lines = (frontmatterMatch[1] || '').split('\n');
359
+
360
+ for (const line of lines) {
361
+ // Skip empty lines
362
+ if (!line.trim()) {
363
+ continue;
364
+ }
365
+
366
+ // Find first colon to split key and value
367
+ const colonIndex = line.indexOf(':');
368
+ if (colonIndex === -1) {
369
+ continue; // Skip lines without colons
370
+ }
371
+
372
+ const key = line.substring(0, colonIndex).trim();
373
+ const value = line.substring(colonIndex + 1).trim();
374
+
375
+ if (key) {
376
+ frontmatter[key] = value;
377
+ }
378
+ }
379
+
380
+ return frontmatter;
381
+ }
382
+
324
383
  /**
325
384
  * Determine dependencies between features
326
385
  *
@@ -604,6 +663,221 @@ ${prdContent}`;
604
663
  yield chunk;
605
664
  }
606
665
  }
666
+
667
+ // ==========================================
668
+ // 6. CLI OPERATIONS (I/O Methods)
669
+ // ==========================================
670
+
671
+ /**
672
+ * List all epics with metadata
673
+ *
674
+ * @returns {Promise<Array<Object>>} Array of epic objects
675
+ */
676
+ async listEpics() {
677
+ const fs = require('fs-extra');
678
+ const path = require('path');
679
+
680
+ const epicsDir = path.join(process.cwd(), this.options.epicsDir);
681
+
682
+ // Check if epics directory exists
683
+ const dirExists = await fs.pathExists(epicsDir);
684
+ if (!dirExists) {
685
+ return [];
686
+ }
687
+
688
+ // Read all epic directories
689
+ let epicDirs;
690
+ try {
691
+ const items = await fs.readdir(epicsDir, { withFileTypes: true });
692
+ epicDirs = items
693
+ .filter(dirent => dirent.isDirectory())
694
+ .map(dirent => dirent.name);
695
+ } catch (error) {
696
+ return [];
697
+ }
698
+
699
+ const epics = [];
700
+
701
+ for (const epicDir of epicDirs) {
702
+ const epicPath = path.join(epicsDir, epicDir);
703
+ const epicFilePath = path.join(epicPath, 'epic.md');
704
+
705
+ // Skip directories without epic.md file
706
+ const fileExists = await fs.pathExists(epicFilePath);
707
+ if (!fileExists) {
708
+ continue;
709
+ }
710
+
711
+ let metadata;
712
+ try {
713
+ const content = await fs.readFile(epicFilePath, 'utf8');
714
+ metadata = this.parseFrontmatter(content);
715
+ } catch (error) {
716
+ // Skip files that can't be read
717
+ continue;
718
+ }
719
+
720
+ // Apply defaults
721
+ const name = (metadata && metadata.name) || epicDir;
722
+ const status = (metadata && metadata.status) || this.options.defaultStatus;
723
+ const progress = (metadata && metadata.progress) || '0%';
724
+ const github = (metadata && metadata.github) || '';
725
+ const created = (metadata && metadata.created) || '';
726
+
727
+ // Count tasks
728
+ const taskCount = await this.countTasks(epicPath);
729
+
730
+ // Extract GitHub issue number
731
+ const githubIssue = this.extractGitHubIssue(github);
732
+
733
+ epics.push({
734
+ name,
735
+ status,
736
+ progress,
737
+ github,
738
+ githubIssue,
739
+ created,
740
+ taskCount,
741
+ epicDir,
742
+ epicPath: path.join(epicPath, 'epic.md')
743
+ });
744
+ }
745
+
746
+ return epics;
747
+ }
748
+
749
+ /**
750
+ * Get detailed epic information
751
+ *
752
+ * @param {string} epicName - Epic directory name
753
+ * @returns {Promise<Object>} Epic data with metadata and task count
754
+ * @throws {Error} If epic not found
755
+ */
756
+ async getEpic(epicName) {
757
+ const fs = require('fs-extra');
758
+
759
+ const epicPath = this.getEpicPath(epicName);
760
+ const epicFilePath = this.getEpicFilePath(epicName);
761
+
762
+ // Check if epic exists
763
+ const exists = await fs.pathExists(epicFilePath);
764
+ if (!exists) {
765
+ throw new Error(`Epic not found: ${epicName}`);
766
+ }
767
+
768
+ // Read epic content
769
+ const content = await fs.readFile(epicFilePath, 'utf8');
770
+ const metadata = this.parseFrontmatter(content);
771
+
772
+ // Apply defaults
773
+ const name = (metadata && metadata.name) || epicName;
774
+ const status = (metadata && metadata.status) || this.options.defaultStatus;
775
+ const progress = (metadata && metadata.progress) || '0%';
776
+ const github = (metadata && metadata.github) || '';
777
+ const created = (metadata && metadata.created) || '';
778
+
779
+ // Count tasks
780
+ const taskCount = await this.countTasks(epicPath);
781
+
782
+ // Extract GitHub issue
783
+ const githubIssue = this.extractGitHubIssue(github);
784
+
785
+ return {
786
+ name,
787
+ status,
788
+ progress,
789
+ github,
790
+ githubIssue,
791
+ created,
792
+ taskCount,
793
+ epicDir: epicName,
794
+ epicPath: epicFilePath,
795
+ content
796
+ };
797
+ }
798
+
799
+ /**
800
+ * Validate epic structure and completeness
801
+ *
802
+ * @param {string} epicName - Epic directory name
803
+ * @returns {Promise<Object>} Validation result: { valid: boolean, issues: string[] }
804
+ * @throws {Error} If epic not found
805
+ */
806
+ async validateEpicStructure(epicName) {
807
+ const fs = require('fs-extra');
808
+
809
+ const epicFilePath = this.getEpicFilePath(epicName);
810
+
811
+ // Check if epic exists
812
+ const exists = await fs.pathExists(epicFilePath);
813
+ if (!exists) {
814
+ throw new Error(`Epic not found: ${epicName}`);
815
+ }
816
+
817
+ const issues = [];
818
+
819
+ // Read epic content
820
+ const content = await fs.readFile(epicFilePath, 'utf8');
821
+
822
+ // Check for frontmatter
823
+ const frontmatter = this.parseFrontmatter(content);
824
+ if (!frontmatter) {
825
+ issues.push('Missing frontmatter');
826
+ } else {
827
+ // Check for required fields
828
+ if (!frontmatter.name) {
829
+ issues.push('Missing name in frontmatter');
830
+ }
831
+ if (!frontmatter.status) {
832
+ issues.push('Missing status in frontmatter');
833
+ }
834
+ }
835
+
836
+ return {
837
+ valid: issues.length === 0,
838
+ issues
839
+ };
840
+ }
841
+
842
+ /**
843
+ * Count task files in epic directory
844
+ *
845
+ * @param {string} epicPath - Path to epic directory
846
+ * @returns {Promise<number>} Number of task files
847
+ */
848
+ async countTasks(epicPath) {
849
+ const fs = require('fs-extra');
850
+
851
+ try {
852
+ const files = await fs.readdir(epicPath);
853
+ // Count files that match pattern [0-9]*.md
854
+ return files.filter(file => /^\d+\.md$/.test(file)).length;
855
+ } catch (error) {
856
+ return 0;
857
+ }
858
+ }
859
+
860
+ /**
861
+ * Get full path to epic directory
862
+ *
863
+ * @param {string} epicName - Epic directory name
864
+ * @returns {string} Full path to epic directory
865
+ */
866
+ getEpicPath(epicName) {
867
+ const path = require('path');
868
+ return path.join(process.cwd(), this.options.epicsDir, epicName);
869
+ }
870
+
871
+ /**
872
+ * Get full path to epic.md file
873
+ *
874
+ * @param {string} epicName - Epic directory name
875
+ * @returns {string} Full path to epic.md file
876
+ */
877
+ getEpicFilePath(epicName) {
878
+ const path = require('path');
879
+ return path.join(this.getEpicPath(epicName), 'epic.md');
880
+ }
607
881
  }
608
882
 
609
883
  module.exports = EpicService;
@@ -880,6 +880,55 @@ ${content}`;
880
880
  // TIER 4: AI STREAMING METHODS
881
881
  // ==========================================
882
882
 
883
+ /**
884
+ * Generate PRD from prompt (non-streaming)
885
+ *
886
+ * Uses AI to generate a complete PRD from a high-level description or prompt.
887
+ * Returns the generated PRD as markdown text.
888
+ *
889
+ * @param {string} prompt - Prompt describing what PRD to generate
890
+ * @param {Object} [options] - Generation options
891
+ * @returns {Promise<string>} Generated PRD markdown
892
+ * @throws {Error} If provider is not available
893
+ *
894
+ * @example
895
+ * const prd = await service.generatePRD('Create a PRD for user authentication');
896
+ * console.log(prd); // Full PRD markdown
897
+ */
898
+ async generatePRD(prompt, options = {}) {
899
+ if (!this.provider || !this.provider.generate) {
900
+ throw new Error('PRD generation requires an AI provider with generate() support');
901
+ }
902
+
903
+ return await this.provider.generate(prompt, options);
904
+ }
905
+
906
+ /**
907
+ * Generate PRD from prompt with streaming output
908
+ *
909
+ * Uses AI to generate a complete PRD from a high-level description,
910
+ * streaming the output in real-time as it's generated.
911
+ *
912
+ * @param {string} prompt - Prompt describing what PRD to generate
913
+ * @param {Object} [options] - Streaming options
914
+ * @returns {AsyncGenerator<string>} Stream of PRD markdown chunks
915
+ * @throws {Error} If provider is not available or lacks stream() support
916
+ *
917
+ * @example
918
+ * for await (const chunk of service.generatePRDStream('Create a PRD for...')) {
919
+ * process.stdout.write(chunk); // Display PRD as it's generated
920
+ * }
921
+ */
922
+ async *generatePRDStream(prompt, options = {}) {
923
+ if (!this.provider || !this.provider.stream) {
924
+ throw new Error('Streaming PRD generation requires an AI provider with stream() support');
925
+ }
926
+
927
+ for await (const chunk of this.provider.stream(prompt, options)) {
928
+ yield chunk;
929
+ }
930
+ }
931
+
883
932
  /**
884
933
  * Parse PRD with streaming AI analysis
885
934
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-autopm",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "description": "Autonomous Project Management Framework for Claude Code - Advanced AI-powered development automation",
5
5
  "main": "bin/autopm.js",
6
6
  "bin": {