@aiready/cli 0.9.45 → 0.9.47

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.
@@ -24,7 +24,7 @@ import {
24
24
  getRepoMetadata,
25
25
  type ToolScoringOutput,
26
26
  } from '@aiready/core';
27
- import { analyzeUnified } from '../index';
27
+ import { analyzeUnified, scoreUnified, type ScoringResult } from '../index';
28
28
  import {
29
29
  getReportTimestamp,
30
30
  warnIfGraphCapExceeded,
@@ -86,11 +86,11 @@ export async function scanAction(directory: string, options: ScanOptions) {
86
86
 
87
87
  let profileTools = options.tools
88
88
  ? options.tools.split(',').map((t: string) => {
89
- const tool = t.trim();
90
- if (tool === 'hallucination' || tool === 'hallucination-risk')
91
- return 'aiSignalClarity';
92
- return tool;
93
- })
89
+ const tool = t.trim();
90
+ if (tool === 'hallucination' || tool === 'hallucination-risk')
91
+ return 'aiSignalClarity';
92
+ return tool;
93
+ })
94
94
  : undefined;
95
95
  if (options.profile) {
96
96
  switch (options.profile.toLowerCase()) {
@@ -488,303 +488,162 @@ export async function scanAction(directory: string, options: ScanOptions) {
488
488
  void elapsedTime;
489
489
 
490
490
  // Calculate score if requested: assemble per-tool scoring outputs
491
- let scoringResult: ReturnType<typeof calculateOverallScore> | undefined;
491
+ let scoringResult: ScoringResult | undefined;
492
492
  if (options.score || finalOptions.scoring?.showBreakdown) {
493
- const toolScores: Map<string, ToolScoringOutput> = new Map();
493
+ scoringResult = await scoreUnified(results, finalOptions);
494
494
 
495
- // Patterns score
496
- if (results.duplicates) {
497
- const { calculatePatternScore } =
498
- await import('@aiready/pattern-detect');
499
- try {
500
- const patternScore = calculatePatternScore(
501
- results.duplicates,
502
- results.patterns?.length || 0
503
- );
504
-
505
- // Calculate token budget for patterns (waste = duplication)
506
- const wastedTokens = results.duplicates.reduce((sum: number, d: any) => sum + (d.tokenCost || 0), 0);
507
- patternScore.tokenBudget = calculateTokenBudget({
508
- totalContextTokens: wastedTokens * 2, // Estimated context
509
- wastedTokens: {
510
- duplication: wastedTokens,
511
- fragmentation: 0,
512
- chattiness: 0
513
- }
514
- });
515
-
516
- toolScores.set('pattern-detect', patternScore);
517
- } catch (err) {
518
- void err;
519
- }
520
- }
521
-
522
- // Context score
523
- if (results.context) {
524
- const { generateSummary: genContextSummary, calculateContextScore } =
525
- await import('@aiready/context-analyzer');
526
- try {
527
- const ctxSummary = genContextSummary(results.context);
528
- const contextScore = calculateContextScore(ctxSummary);
529
-
530
- // Calculate token budget for context (waste = fragmentation + depth overhead)
531
- contextScore.tokenBudget = calculateTokenBudget({
532
- totalContextTokens: ctxSummary.totalTokens,
533
- wastedTokens: {
534
- duplication: 0,
535
- fragmentation: ctxSummary.totalPotentialSavings || 0,
536
- chattiness: 0
537
- }
538
- });
495
+ console.log(chalk.bold('\nšŸ“Š AI Readiness Overall Score'));
496
+ console.log(` ${formatScore(scoringResult)}`);
539
497
 
540
- toolScores.set('context-analyzer', contextScore);
541
- } catch (err) {
542
- void err;
543
- }
544
- }
545
-
546
- // Consistency score
547
- if (results.consistency) {
548
- const { calculateConsistencyScore } =
549
- await import('@aiready/consistency');
550
- try {
551
- const issues =
552
- results.consistency.results?.flatMap((r: any) => r.issues) || [];
553
- const totalFiles = results.consistency.summary?.filesAnalyzed || 0;
554
- const consistencyScore = calculateConsistencyScore(
555
- issues,
556
- totalFiles
557
- );
558
- toolScores.set('consistency', consistencyScore);
559
- } catch (err) {
560
- void err;
561
- }
562
- }
498
+ // Parse CLI weight overrides (if any)
499
+ // Note: weights are already handled inside scoreUnified via finalOptions and calculateOverallScore
563
500
 
564
- // AI signal clarity score
565
- if (results.aiSignalClarity) {
566
- const { calculateAiSignalClarityScore } =
567
- await import('@aiready/ai-signal-clarity');
501
+ // Check if we need to compare to a previous report
502
+ if (options.compareTo) {
568
503
  try {
569
- const hrScore = calculateAiSignalClarityScore(
570
- results.aiSignalClarity
504
+ const prevReportStr = readFileSync(
505
+ resolvePath(process.cwd(), options.compareTo),
506
+ 'utf8'
571
507
  );
572
- toolScores.set('ai-signal-clarity', hrScore);
573
- } catch (err) {
574
- void err;
575
- }
576
- }
508
+ const prevReport = JSON.parse(prevReportStr);
509
+ const prevScore =
510
+ prevReport.scoring?.score || prevReport.scoring?.overallScore;
577
511
 
578
- // Agent grounding score
579
- if (results.grounding) {
580
- const { calculateGroundingScore } =
581
- await import('@aiready/agent-grounding');
582
- try {
583
- const agScore = calculateGroundingScore(results.grounding);
584
- toolScores.set('agent-grounding', agScore);
585
- } catch (err) {
586
- void err;
587
- }
588
- }
589
-
590
- // Testability score
591
- if (results.testability) {
592
- const { calculateTestabilityScore } =
593
- await import('@aiready/testability');
594
- try {
595
- const tbScore = calculateTestabilityScore(results.testability);
596
- toolScores.set('testability', tbScore);
597
- } catch (err) {
598
- void err;
599
- }
600
- }
601
-
602
- // Documentation Drift score
603
- if (results.docDrift) {
604
- toolScores.set('doc-drift', {
605
- toolName: 'doc-drift',
606
- score: results.docDrift.summary.score,
607
- rawMetrics: results.docDrift.rawData,
608
- factors: [],
609
- recommendations: (results.docDrift.recommendations || []).map(
610
- (action: string) => ({
611
- action,
612
- estimatedImpact: 5,
613
- priority: 'medium',
614
- })
615
- ),
616
- });
617
- }
618
-
619
- // Dependency Health score
620
- if (results.deps) {
621
- toolScores.set('dependency-health', {
622
- toolName: 'dependency-health',
623
- score: results.deps.summary.score,
624
- rawMetrics: results.deps.rawData,
625
- factors: [],
626
- recommendations: (results.deps.recommendations || []).map(
627
- (action: string) => ({
628
- action,
629
- estimatedImpact: 5,
630
- priority: 'medium',
631
- })
632
- ),
633
- });
634
- }
635
-
636
- // Change Amplification score
637
- if (results.changeAmplification) {
638
- toolScores.set('change-amplification', {
639
- toolName: 'change-amplification',
640
- score: results.changeAmplification.summary.score,
641
- rawMetrics: results.changeAmplification.rawData,
642
- factors: [],
643
- recommendations: (
644
- results.changeAmplification.recommendations || []
645
- ).map((action: string) => ({
646
- action,
647
- estimatedImpact: 5,
648
- priority: 'medium',
649
- })),
650
- });
651
- }
652
-
653
- // Parse CLI weight overrides (if any)
654
- const cliWeights = parseWeightString((options as any).weights);
655
-
656
- // Only calculate overall score if we have at least one tool score
657
- if (toolScores.size > 0) {
658
- scoringResult = calculateOverallScore(
659
- toolScores,
660
- finalOptions,
661
- cliWeights.size ? cliWeights : undefined
662
- );
663
-
664
- console.log(chalk.bold('\nšŸ“Š AI Readiness Overall Score'));
665
- console.log(` ${formatScore(scoringResult)}`);
666
-
667
- // Check if we need to compare to a previous report
668
- if (options.compareTo) {
669
- try {
670
- const prevReportStr = readFileSync(
671
- resolvePath(process.cwd(), options.compareTo),
672
- 'utf8'
673
- );
674
- const prevReport = JSON.parse(prevReportStr);
675
- const prevScore =
676
- prevReport.scoring?.score || prevReport.scoring?.overallScore;
677
-
678
- if (typeof prevScore === 'number') {
679
- const diff = scoringResult.overall - prevScore;
680
- const diffStr = diff > 0 ? `+${diff}` : String(diff);
681
- console.log();
682
- if (diff > 0) {
683
- console.log(
684
- chalk.green(
685
- ` šŸ“ˆ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
686
- )
687
- );
688
- } else if (diff < 0) {
689
- console.log(
690
- chalk.red(
691
- ` šŸ“‰ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
692
- )
693
- );
694
- // Trend gating: if we regressed and CI is on or threshold is present, we could lower the threshold effectively,
695
- // but for now, we just highlight the regression.
696
- } else {
697
- console.log(
698
- chalk.blue(
699
- ` āž– Trend: No change compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
700
- )
701
- );
702
- }
703
-
704
- // Add trend info to scoringResult for programmatic use
705
- (scoringResult as any).trend = {
706
- previousScore: prevScore,
707
- difference: diff,
708
- };
512
+ if (typeof prevScore === 'number') {
513
+ const diff = scoringResult.overall - prevScore;
514
+ const diffStr = diff > 0 ? `+${diff}` : String(diff);
515
+ console.log();
516
+ if (diff > 0) {
517
+ console.log(
518
+ chalk.green(
519
+ ` šŸ“ˆ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
520
+ )
521
+ );
522
+ } else if (diff < 0) {
523
+ console.log(
524
+ chalk.red(
525
+ ` šŸ“‰ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
526
+ )
527
+ );
528
+ // Trend gating: if we regressed and CI is on or threshold is present, we could lower the threshold effectively,
529
+ // but for now, we just highlight the regression.
709
530
  } else {
710
531
  console.log(
711
- chalk.yellow(
712
- `\n āš ļø Previous report at ${options.compareTo} does not contain an overall score.`
532
+ chalk.blue(
533
+ ` āž– Trend: No change compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
713
534
  )
714
535
  );
715
536
  }
716
- } catch (e) {
717
- void e;
537
+
538
+ // Add trend info to scoringResult for programmatic use
539
+ (scoringResult as any).trend = {
540
+ previousScore: prevScore,
541
+ difference: diff,
542
+ };
543
+ } else {
718
544
  console.log(
719
545
  chalk.yellow(
720
- `\n āš ļø Could not read or parse previous report at ${options.compareTo}.`
546
+ `\n āš ļø Previous report at ${options.compareTo} does not contain an overall score.`
721
547
  )
722
548
  );
723
549
  }
550
+ } catch (e) {
551
+ void e;
552
+ console.log(
553
+ chalk.yellow(
554
+ `\n āš ļø Could not read or parse previous report at ${options.compareTo}.`
555
+ )
556
+ );
724
557
  }
558
+ }
725
559
 
726
- // Unified Token Budget Analysis
727
- const totalWastedDuplication = Array.from(toolScores.values())
728
- .reduce((sum, s) => sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0), 0);
729
- const totalWastedFragmentation = Array.from(toolScores.values())
730
- .reduce((sum, s) => sum + (s.tokenBudget?.wastedTokens.bySource.fragmentation || 0), 0);
731
- const totalContext = Math.max(...Array.from(toolScores.values()).map(s => s.tokenBudget?.totalContextTokens || 0));
732
-
733
- if (totalContext > 0) {
734
- const unifiedBudget = calculateTokenBudget({
735
- totalContextTokens: totalContext,
736
- wastedTokens: {
737
- duplication: totalWastedDuplication,
738
- fragmentation: totalWastedFragmentation,
739
- chattiness: 0
740
- }
741
- });
560
+ // Unified Token Budget Analysis
561
+ const totalWastedDuplication = (scoringResult.breakdown || []).reduce(
562
+ (sum, s) =>
563
+ sum + (s.tokenBudget?.wastedTokens.bySource.duplication || 0),
564
+ 0
565
+ );
566
+ const totalWastedFragmentation = (scoringResult.breakdown || []).reduce(
567
+ (sum, s) =>
568
+ sum + (s.tokenBudget?.wastedTokens.bySource.fragmentation || 0),
569
+ 0
570
+ );
571
+ const totalContext = Math.max(
572
+ ...(scoringResult.breakdown || []).map(
573
+ (s) => s.tokenBudget?.totalContextTokens || 0
574
+ )
575
+ );
742
576
 
743
- const targetModel = options.model || 'claude-4.6';
744
- const modelPreset = getModelPreset(targetModel);
745
- const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
746
-
747
- const barWidth = 20;
748
- const filled = Math.round(unifiedBudget.efficiencyRatio * barWidth);
749
- const bar = chalk.green('ā–ˆ'.repeat(filled)) + chalk.dim('ā–‘'.repeat(barWidth - filled));
750
-
751
- console.log(chalk.bold('\nšŸ“Š AI Token Budget Analysis (v0.13)'));
752
- console.log(` Efficiency: [${bar}] ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`);
753
- console.log(` Total Context: ${chalk.bold(unifiedBudget.totalContextTokens.toLocaleString())} tokens`);
754
- console.log(` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())} (${((unifiedBudget.wastedTokens.total / unifiedBudget.totalContextTokens) * 100).toFixed(1)}%)`);
755
- console.log(` Waste Breakdown:`);
756
- console.log(` • Duplication: ${unifiedBudget.wastedTokens.bySource.duplication.toLocaleString()} tokens`);
757
- console.log(` • Fragmentation: ${unifiedBudget.wastedTokens.bySource.fragmentation.toLocaleString()} tokens`);
758
- console.log(` Potential Savings: ${chalk.green(unifiedBudget.potentialRetrievableTokens.toLocaleString())} tokens retrievable`);
759
- console.log(`\n Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)} [range: $${costEstimate.range[0]}-$${costEstimate.range[1]}]`);
760
-
761
- // Attach unified budget to report for JSON persistence
762
- (scoringResult as any).tokenBudget = unifiedBudget;
763
- (scoringResult as any).costEstimate = {
764
- model: modelPreset.name,
765
- ...costEstimate
766
- };
767
- }
577
+ if (totalContext > 0) {
578
+ const unifiedBudget = calculateTokenBudget({
579
+ totalContextTokens: totalContext,
580
+ wastedTokens: {
581
+ duplication: totalWastedDuplication,
582
+ fragmentation: totalWastedFragmentation,
583
+ chattiness: 0,
584
+ },
585
+ });
586
+
587
+ const targetModel = options.model || 'claude-4.6';
588
+ const modelPreset = getModelPreset(targetModel);
589
+ const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
590
+
591
+ const barWidth = 20;
592
+ const filled = Math.round(unifiedBudget.efficiencyRatio * barWidth);
593
+ const bar =
594
+ chalk.green('ā–ˆ'.repeat(filled)) +
595
+ chalk.dim('ā–‘'.repeat(barWidth - filled));
596
+
597
+ console.log(chalk.bold('\nšŸ“Š AI Token Budget Analysis (v0.13)'));
598
+ console.log(
599
+ ` Efficiency: [${bar}] ${(unifiedBudget.efficiencyRatio * 100).toFixed(0)}%`
600
+ );
601
+ console.log(
602
+ ` Total Context: ${chalk.bold(unifiedBudget.totalContextTokens.toLocaleString())} tokens`
603
+ );
604
+ console.log(
605
+ ` Wasted Tokens: ${chalk.red(unifiedBudget.wastedTokens.total.toLocaleString())} (${((unifiedBudget.wastedTokens.total / unifiedBudget.totalContextTokens) * 100).toFixed(1)}%)`
606
+ );
607
+ console.log(` Waste Breakdown:`);
608
+ console.log(
609
+ ` • Duplication: ${unifiedBudget.wastedTokens.bySource.duplication.toLocaleString()} tokens`
610
+ );
611
+ console.log(
612
+ ` • Fragmentation: ${unifiedBudget.wastedTokens.bySource.fragmentation.toLocaleString()} tokens`
613
+ );
614
+ console.log(
615
+ ` Potential Savings: ${chalk.green(unifiedBudget.potentialRetrievableTokens.toLocaleString())} tokens retrievable`
616
+ );
617
+ console.log(
618
+ `\n Est. Monthly Cost (${modelPreset.name}): ${chalk.bold('$' + costEstimate.total)} [range: $${costEstimate.range[0]}-$${costEstimate.range[1]}]`
619
+ );
768
620
 
769
- // Show concise breakdown; detailed breakdown only if config requests it
770
- if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
771
- console.log(chalk.bold('\nTool breakdown:'));
621
+ // Attach unified budget to report for JSON persistence
622
+ (scoringResult as any).tokenBudget = unifiedBudget;
623
+ (scoringResult as any).costEstimate = {
624
+ model: modelPreset.name,
625
+ ...costEstimate,
626
+ };
627
+ }
628
+
629
+ // Show concise breakdown; detailed breakdown only if config requests it
630
+ if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
631
+ console.log(chalk.bold('\nTool breakdown:'));
632
+ scoringResult.breakdown.forEach((tool) => {
633
+ const rating = getRating(tool.score);
634
+ const rd = getRatingDisplay(rating);
635
+ console.log(
636
+ ` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
637
+ );
638
+ });
639
+ console.log();
640
+
641
+ if (finalOptions.scoring?.showBreakdown) {
642
+ console.log(chalk.bold('Detailed tool breakdown:'));
772
643
  scoringResult.breakdown.forEach((tool) => {
773
- const rating = getRating(tool.score);
774
- const rd = getRatingDisplay(rating);
775
- console.log(
776
- ` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
777
- );
644
+ console.log(formatToolScore(tool));
778
645
  });
779
646
  console.log();
780
-
781
- if (finalOptions.scoring?.showBreakdown) {
782
- console.log(chalk.bold('Detailed tool breakdown:'));
783
- scoringResult.breakdown.forEach((tool) => {
784
- console.log(formatToolScore(tool));
785
- });
786
- console.log();
787
- }
788
647
  }
789
648
  }
790
649
  }
@@ -1,9 +1,7 @@
1
1
  import fs from 'fs';
2
- import path, { resolve as resolvePath } from 'path';
2
+ import { resolve as resolvePath } from 'path';
3
3
  import chalk from 'chalk';
4
- import {
5
- handleCLIError,
6
- } from '@aiready/core';
4
+ import { handleCLIError } from '@aiready/core';
7
5
 
8
6
  interface UploadOptions {
9
7
  apiKey?: string;
@@ -14,13 +12,22 @@ interface UploadOptions {
14
12
  export async function uploadAction(file: string, options: UploadOptions) {
15
13
  const startTime = Date.now();
16
14
  const filePath = resolvePath(process.cwd(), file);
17
- const serverUrl = options.server || process.env.AIREADY_SERVER || 'https://dev.platform.getaiready.dev';
15
+ const serverUrl =
16
+ options.server ||
17
+ process.env.AIREADY_SERVER ||
18
+ 'https://dev.platform.getaiready.dev';
18
19
  const apiKey = options.apiKey || process.env.AIREADY_API_KEY;
19
20
 
20
21
  if (!apiKey) {
21
22
  console.error(chalk.red('āŒ API Key is required for upload.'));
22
- console.log(chalk.dim(' Set AIREADY_API_KEY environment variable or use --api-key flag.'));
23
- console.log(chalk.dim(' Get an API key from https://getaiready.dev/dashboard'));
23
+ console.log(
24
+ chalk.dim(
25
+ ' Set AIREADY_API_KEY environment variable or use --api-key flag.'
26
+ )
27
+ );
28
+ console.log(
29
+ chalk.dim(' Get an API key from https://getaiready.dev/dashboard')
30
+ );
24
31
  process.exit(1);
25
32
  }
26
33
 
@@ -33,7 +40,10 @@ export async function uploadAction(file: string, options: UploadOptions) {
33
40
  console.log(chalk.blue(`šŸš€ Uploading report to ${serverUrl}...`));
34
41
 
35
42
  // Read the report file
36
- const reportData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
43
+ console.log(chalk.dim(` Reading report from ${filePath}...`));
44
+ const reportContent = fs.readFileSync(filePath, 'utf-8');
45
+ const reportData = JSON.parse(reportContent);
46
+ console.log(chalk.dim(` Successfully parsed report JSON.`));
37
47
 
38
48
  // Prepare upload payload
39
49
  // Note: repoId is optional if the metadata contains it, but for now we'll require it or infer from metadata
@@ -43,7 +53,7 @@ export async function uploadAction(file: string, options: UploadOptions) {
43
53
  method: 'POST',
44
54
  headers: {
45
55
  'Content-Type': 'application/json',
46
- 'Authorization': `Bearer ${apiKey}`,
56
+ Authorization: `Bearer ${apiKey}`,
47
57
  },
48
58
  body: JSON.stringify({
49
59
  data: reportData,
@@ -51,12 +61,41 @@ export async function uploadAction(file: string, options: UploadOptions) {
51
61
  }),
52
62
  });
53
63
 
54
- const result = (await res.json()) as any;
64
+ const contentType = res.headers.get('content-type');
65
+ let result: any = {};
66
+
67
+ if (contentType?.includes('application/json')) {
68
+ result = await res.json();
69
+ } else {
70
+ const text = await res.text();
71
+ result = { error: text || res.statusText };
72
+ }
55
73
 
56
74
  if (!res.ok) {
57
- console.error(chalk.red(`āŒ Upload failed: ${result.error || res.statusText}`));
75
+ console.error(
76
+ chalk.red(`āŒ Upload failed: ${result.error || res.statusText}`)
77
+ );
78
+
79
+ // Special case for redirects or HTML error pages
80
+ if (contentType?.includes('text/html')) {
81
+ console.log(
82
+ chalk.yellow(
83
+ ' Note: Received an HTML response. This often indicates a redirect (e.g., to a login page) or a server error.'
84
+ )
85
+ );
86
+ if (result.error?.includes('Redirecting')) {
87
+ console.log(
88
+ chalk.dim(
89
+ ' Detected redirect. Check if the API endpoint requires authentication or has changed.'
90
+ )
91
+ );
92
+ }
93
+ }
94
+
58
95
  if (res.status === 401) {
59
- console.log(chalk.dim(' Hint: Your API key may be invalid or expired.'));
96
+ console.log(
97
+ chalk.dim(' Hint: Your API key may be invalid or expired.')
98
+ );
60
99
  }
61
100
  process.exit(1);
62
101
  }
@@ -69,7 +108,6 @@ export async function uploadAction(file: string, options: UploadOptions) {
69
108
  console.log(chalk.dim(` Analysis ID: ${result.analysis.id}`));
70
109
  console.log(chalk.dim(` Score: ${result.analysis.aiScore}/100`));
71
110
  }
72
-
73
111
  } catch (error) {
74
112
  handleCLIError(error, 'Upload');
75
113
  }