@aiready/cli 0.9.43 → 0.9.46

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/src/cli.ts CHANGED
@@ -16,6 +16,8 @@ import {
16
16
  visualizeHelpText,
17
17
  visualiseHelpText,
18
18
  changeAmplificationAction,
19
+ uploadAction,
20
+ uploadHelpText,
19
21
  } from './commands';
20
22
 
21
23
  const getDirname = () => {
@@ -111,6 +113,9 @@ program
111
113
  'Fail on issues: critical, major, any',
112
114
  'critical'
113
115
  )
116
+ .option('--api-key <key>', 'Platform API key for automatic upload')
117
+ .option('--upload', 'Automatically upload results to the platform')
118
+ .option('--server <url>', 'Custom platform URL')
114
119
  .addHelpText('after', scanHelpText)
115
120
  .action(async (directory, options) => {
116
121
  await scanAction(directory, options);
@@ -274,4 +279,17 @@ program
274
279
  await changeAmplificationAction(directory, options);
275
280
  });
276
281
 
282
+ // Upload command - Upload report JSON to platform
283
+ program
284
+ .command('upload')
285
+ .description('Upload an AIReady report JSON to the platform')
286
+ .argument('<file>', 'Report JSON file to upload')
287
+ .option('--api-key <key>', 'Platform API key')
288
+ .option('--repo-id <id>', 'Platform repository ID (optional)')
289
+ .option('--server <url>', 'Custom platform URL')
290
+ .addHelpText('after', uploadHelpText)
291
+ .action(async (file, options) => {
292
+ await uploadAction(file, options);
293
+ });
294
+
277
295
  program.parse();
@@ -15,3 +15,4 @@ export { aiSignalClarityAction } from './ai-signal-clarity';
15
15
  export { agentGroundingAction } from './agent-grounding';
16
16
  export { testabilityAction } from './testability';
17
17
  export { changeAmplificationAction } from './change-amplification';
18
+ export { uploadAction, uploadHelpText } from './upload';
@@ -15,17 +15,22 @@ import {
15
15
  calculateOverallScore,
16
16
  formatScore,
17
17
  formatToolScore,
18
+ calculateTokenBudget,
19
+ estimateCostFromBudget,
20
+ getModelPreset,
18
21
  getRating,
19
22
  getRatingDisplay,
20
23
  parseWeightString,
24
+ getRepoMetadata,
21
25
  type ToolScoringOutput,
22
26
  } from '@aiready/core';
23
- import { analyzeUnified } from '../index';
27
+ import { analyzeUnified, scoreUnified, type ScoringResult } from '../index';
24
28
  import {
25
29
  getReportTimestamp,
26
30
  warnIfGraphCapExceeded,
27
31
  truncateArray,
28
32
  } from '../utils/helpers';
33
+ import { uploadAction } from './upload';
29
34
 
30
35
  interface ScanOptions {
31
36
  tools?: string;
@@ -41,6 +46,10 @@ interface ScanOptions {
41
46
  threshold?: string;
42
47
  ci?: boolean;
43
48
  failOn?: string;
49
+ model?: string;
50
+ apiKey?: string;
51
+ upload?: boolean;
52
+ server?: string;
44
53
  }
45
54
 
46
55
  export async function scanAction(directory: string, options: ScanOptions) {
@@ -50,6 +59,9 @@ export async function scanAction(directory: string, options: ScanOptions) {
50
59
  // Resolve directory to absolute path to ensure .aiready/ is created in the right location
51
60
  const resolvedDir = resolvePath(process.cwd(), directory || '.');
52
61
 
62
+ // Extract repo metadata for linkage
63
+ const repoMetadata = getRepoMetadata(resolvedDir);
64
+
53
65
  try {
54
66
  // Define defaults
55
67
  const defaults = {
@@ -476,237 +488,162 @@ export async function scanAction(directory: string, options: ScanOptions) {
476
488
  void elapsedTime;
477
489
 
478
490
  // Calculate score if requested: assemble per-tool scoring outputs
479
- let scoringResult: ReturnType<typeof calculateOverallScore> | undefined;
491
+ let scoringResult: ScoringResult | undefined;
480
492
  if (options.score || finalOptions.scoring?.showBreakdown) {
481
- const toolScores: Map<string, ToolScoringOutput> = new Map();
493
+ scoringResult = await scoreUnified(results, finalOptions);
482
494
 
483
- // Patterns score
484
- if (results.duplicates) {
485
- const { calculatePatternScore } =
486
- await import('@aiready/pattern-detect');
487
- try {
488
- const patternScore = calculatePatternScore(
489
- results.duplicates,
490
- results.patterns?.length || 0
491
- );
492
- toolScores.set('pattern-detect', patternScore);
493
- } catch (err) {
494
- void err;
495
- }
496
- }
495
+ console.log(chalk.bold('\nšŸ“Š AI Readiness Overall Score'));
496
+ console.log(` ${formatScore(scoringResult)}`);
497
497
 
498
- // Context score
499
- if (results.context) {
500
- const { generateSummary: genContextSummary, calculateContextScore } =
501
- await import('@aiready/context-analyzer');
502
- try {
503
- const ctxSummary = genContextSummary(results.context);
504
- const contextScore = calculateContextScore(ctxSummary);
505
- toolScores.set('context-analyzer', contextScore);
506
- } catch (err) {
507
- void err;
508
- }
509
- }
498
+ // Parse CLI weight overrides (if any)
499
+ // Note: weights are already handled inside scoreUnified via finalOptions and calculateOverallScore
510
500
 
511
- // Consistency score
512
- if (results.consistency) {
513
- const { calculateConsistencyScore } =
514
- await import('@aiready/consistency');
501
+ // Check if we need to compare to a previous report
502
+ if (options.compareTo) {
515
503
  try {
516
- const issues =
517
- results.consistency.results?.flatMap((r: any) => r.issues) || [];
518
- const totalFiles = results.consistency.summary?.filesAnalyzed || 0;
519
- const consistencyScore = calculateConsistencyScore(
520
- issues,
521
- totalFiles
504
+ const prevReportStr = readFileSync(
505
+ resolvePath(process.cwd(), options.compareTo),
506
+ 'utf8'
522
507
  );
523
- toolScores.set('consistency', consistencyScore);
524
- } catch (err) {
525
- void err;
526
- }
527
- }
508
+ const prevReport = JSON.parse(prevReportStr);
509
+ const prevScore =
510
+ prevReport.scoring?.score || prevReport.scoring?.overallScore;
528
511
 
529
- // AI signal clarity score
530
- if (results.aiSignalClarity) {
531
- const { calculateAiSignalClarityScore } =
532
- await import('@aiready/ai-signal-clarity');
533
- try {
534
- const hrScore = calculateAiSignalClarityScore(
535
- results.aiSignalClarity
536
- );
537
- toolScores.set('ai-signal-clarity', hrScore);
538
- } catch (err) {
539
- void err;
540
- }
541
- }
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.
530
+ } else {
531
+ console.log(
532
+ chalk.blue(
533
+ ` āž– Trend: No change compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
534
+ )
535
+ );
536
+ }
542
537
 
543
- // Agent grounding score
544
- if (results.grounding) {
545
- const { calculateGroundingScore } =
546
- await import('@aiready/agent-grounding');
547
- try {
548
- const agScore = calculateGroundingScore(results.grounding);
549
- toolScores.set('agent-grounding', agScore);
550
- } catch (err) {
551
- void err;
538
+ // Add trend info to scoringResult for programmatic use
539
+ (scoringResult as any).trend = {
540
+ previousScore: prevScore,
541
+ difference: diff,
542
+ };
543
+ } else {
544
+ console.log(
545
+ chalk.yellow(
546
+ `\n āš ļø Previous report at ${options.compareTo} does not contain an overall score.`
547
+ )
548
+ );
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
+ );
552
557
  }
553
558
  }
554
559
 
555
- // Testability score
556
- if (results.testability) {
557
- const { calculateTestabilityScore } =
558
- await import('@aiready/testability');
559
- try {
560
- const tbScore = calculateTestabilityScore(results.testability);
561
- toolScores.set('testability', tbScore);
562
- } catch (err) {
563
- void err;
564
- }
565
- }
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
+ );
566
576
 
567
- // Documentation Drift score
568
- if (results.docDrift) {
569
- toolScores.set('doc-drift', {
570
- toolName: 'doc-drift',
571
- score: results.docDrift.summary.score,
572
- rawMetrics: results.docDrift.rawData,
573
- factors: [],
574
- recommendations: (results.docDrift.recommendations || []).map(
575
- (action: string) => ({
576
- action,
577
- estimatedImpact: 5,
578
- priority: 'medium',
579
- })
580
- ),
577
+ if (totalContext > 0) {
578
+ const unifiedBudget = calculateTokenBudget({
579
+ totalContextTokens: totalContext,
580
+ wastedTokens: {
581
+ duplication: totalWastedDuplication,
582
+ fragmentation: totalWastedFragmentation,
583
+ chattiness: 0,
584
+ },
581
585
  });
582
- }
583
586
 
584
- // Dependency Health score
585
- if (results.deps) {
586
- toolScores.set('dependency-health', {
587
- toolName: 'dependency-health',
588
- score: results.deps.summary.score,
589
- rawMetrics: results.deps.rawData,
590
- factors: [],
591
- recommendations: (results.deps.recommendations || []).map(
592
- (action: string) => ({
593
- action,
594
- estimatedImpact: 5,
595
- priority: 'medium',
596
- })
597
- ),
598
- });
599
- }
587
+ const targetModel = options.model || 'claude-4.6';
588
+ const modelPreset = getModelPreset(targetModel);
589
+ const costEstimate = estimateCostFromBudget(unifiedBudget, modelPreset);
600
590
 
601
- // Change Amplification score
602
- if (results.changeAmplification) {
603
- toolScores.set('change-amplification', {
604
- toolName: 'change-amplification',
605
- score: results.changeAmplification.summary.score,
606
- rawMetrics: results.changeAmplification.rawData,
607
- factors: [],
608
- recommendations: (
609
- results.changeAmplification.recommendations || []
610
- ).map((action: string) => ({
611
- action,
612
- estimatedImpact: 5,
613
- priority: 'medium',
614
- })),
615
- });
616
- }
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));
617
596
 
618
- // Parse CLI weight overrides (if any)
619
- const cliWeights = parseWeightString((options as any).weights);
620
-
621
- // Only calculate overall score if we have at least one tool score
622
- if (toolScores.size > 0) {
623
- scoringResult = calculateOverallScore(
624
- toolScores,
625
- finalOptions,
626
- cliWeights.size ? cliWeights : undefined
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]}]`
627
619
  );
628
620
 
629
- console.log(chalk.bold('\nšŸ“Š AI Readiness Overall Score'));
630
- console.log(` ${formatScore(scoringResult)}`);
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
+ }
631
628
 
632
- // Check if we need to compare to a previous report
633
- if (options.compareTo) {
634
- try {
635
- const prevReportStr = readFileSync(
636
- resolvePath(process.cwd(), options.compareTo),
637
- 'utf8'
638
- );
639
- const prevReport = JSON.parse(prevReportStr);
640
- const prevScore =
641
- prevReport.scoring?.score || prevReport.scoring?.overallScore;
642
-
643
- if (typeof prevScore === 'number') {
644
- const diff = scoringResult.overall - prevScore;
645
- const diffStr = diff > 0 ? `+${diff}` : String(diff);
646
- console.log();
647
- if (diff > 0) {
648
- console.log(
649
- chalk.green(
650
- ` šŸ“ˆ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
651
- )
652
- );
653
- } else if (diff < 0) {
654
- console.log(
655
- chalk.red(
656
- ` šŸ“‰ Trend: ${diffStr} compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
657
- )
658
- );
659
- // Trend gating: if we regressed and CI is on or threshold is present, we could lower the threshold effectively,
660
- // but for now, we just highlight the regression.
661
- } else {
662
- console.log(
663
- chalk.blue(
664
- ` āž– Trend: No change compared to ${options.compareTo} (${prevScore} → ${scoringResult.overall})`
665
- )
666
- );
667
- }
668
-
669
- // Add trend info to scoringResult for programmatic use
670
- (scoringResult as any).trend = {
671
- previousScore: prevScore,
672
- difference: diff,
673
- };
674
- } else {
675
- console.log(
676
- chalk.yellow(
677
- `\n āš ļø Previous report at ${options.compareTo} does not contain an overall score.`
678
- )
679
- );
680
- }
681
- } catch (e) {
682
- void e;
683
- console.log(
684
- chalk.yellow(
685
- `\n āš ļø Could not read or parse previous report at ${options.compareTo}.`
686
- )
687
- );
688
- }
689
- }
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();
690
640
 
691
- // Show concise breakdown; detailed breakdown only if config requests it
692
- if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
693
- console.log(chalk.bold('\nTool breakdown:'));
641
+ if (finalOptions.scoring?.showBreakdown) {
642
+ console.log(chalk.bold('Detailed tool breakdown:'));
694
643
  scoringResult.breakdown.forEach((tool) => {
695
- const rating = getRating(tool.score);
696
- const rd = getRatingDisplay(rating);
697
- console.log(
698
- ` - ${tool.toolName}: ${tool.score}/100 (${rating}) ${rd.emoji}`
699
- );
644
+ console.log(formatToolScore(tool));
700
645
  });
701
646
  console.log();
702
-
703
- if (finalOptions.scoring?.showBreakdown) {
704
- console.log(chalk.bold('Detailed tool breakdown:'));
705
- scoringResult.breakdown.forEach((tool) => {
706
- console.log(formatToolScore(tool));
707
- });
708
- console.log();
709
- }
710
647
  }
711
648
  }
712
649
  }
@@ -723,13 +660,26 @@ export async function scanAction(directory: string, options: ScanOptions) {
723
660
  defaultFilename,
724
661
  resolvedDir
725
662
  );
726
- const outputData = { ...results, scoring: scoringResult };
663
+ const outputData = {
664
+ ...results,
665
+ scoring: scoringResult,
666
+ repository: repoMetadata,
667
+ };
727
668
  handleJSONOutput(
728
669
  outputData,
729
670
  outputPath,
730
671
  `āœ… Report saved to ${outputPath}`
731
672
  );
732
673
 
674
+ // Automatic Upload
675
+ if (options.upload) {
676
+ console.log(chalk.blue('\nšŸ“¤ Automatic upload triggered...'));
677
+ await uploadAction(outputPath, {
678
+ apiKey: options.apiKey,
679
+ server: options.server,
680
+ });
681
+ }
682
+
733
683
  // Warn if graph caps may be exceeded
734
684
  await warnIfGraphCapExceeded(outputData, resolvedDir);
735
685
  } else {
@@ -741,11 +691,24 @@ export async function scanAction(directory: string, options: ScanOptions) {
741
691
  defaultFilename,
742
692
  resolvedDir
743
693
  );
744
- const outputData = { ...results, scoring: scoringResult };
694
+ const outputData = {
695
+ ...results,
696
+ scoring: scoringResult,
697
+ repository: repoMetadata,
698
+ };
745
699
 
746
700
  try {
747
701
  writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
748
702
  console.log(chalk.dim(`āœ… Report auto-persisted to ${outputPath}`));
703
+
704
+ // Automatic Upload (from auto-persistent report)
705
+ if (options.upload) {
706
+ console.log(chalk.blue('\nšŸ“¤ Automatic upload triggered...'));
707
+ await uploadAction(outputPath, {
708
+ apiKey: options.apiKey,
709
+ server: options.server,
710
+ });
711
+ }
749
712
  // Warn if graph caps may be exceeded
750
713
  await warnIfGraphCapExceeded(outputData, resolvedDir);
751
714
  } catch (err) {
@@ -897,6 +860,8 @@ EXAMPLES:
897
860
  $ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
898
861
  $ aiready scan --ci --fail-on major # Fail on major+ issues
899
862
  $ aiready scan --output json --output-file report.json
863
+ $ aiready scan --upload --api-key ar_... # Automatic platform upload
864
+ $ aiready scan --upload --server custom-url.com # Upload to custom platform
900
865
 
901
866
  PROFILES:
902
867
  agentic: aiSignalClarity, grounding, testability
@@ -0,0 +1,125 @@
1
+ import fs from 'fs';
2
+ import { resolve as resolvePath } from 'path';
3
+ import chalk from 'chalk';
4
+ import { handleCLIError } from '@aiready/core';
5
+
6
+ interface UploadOptions {
7
+ apiKey?: string;
8
+ repoId?: string;
9
+ server?: string;
10
+ }
11
+
12
+ export async function uploadAction(file: string, options: UploadOptions) {
13
+ const startTime = Date.now();
14
+ const filePath = resolvePath(process.cwd(), file);
15
+ const serverUrl =
16
+ options.server ||
17
+ process.env.AIREADY_SERVER ||
18
+ 'https://dev.platform.getaiready.dev';
19
+ const apiKey = options.apiKey || process.env.AIREADY_API_KEY;
20
+
21
+ if (!apiKey) {
22
+ console.error(chalk.red('āŒ API Key is required for upload.'));
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
+ );
31
+ process.exit(1);
32
+ }
33
+
34
+ if (!fs.existsSync(filePath)) {
35
+ console.error(chalk.red(`āŒ File not found: ${filePath}`));
36
+ process.exit(1);
37
+ }
38
+
39
+ try {
40
+ console.log(chalk.blue(`šŸš€ Uploading report to ${serverUrl}...`));
41
+
42
+ // Read the report file
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.`));
47
+
48
+ // Prepare upload payload
49
+ // Note: repoId is optional if the metadata contains it, but for now we'll require it or infer from metadata
50
+ const repoId = options.repoId || reportData.repository?.repoId;
51
+
52
+ const res = await fetch(`${serverUrl}/api/analysis/upload`, {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ Authorization: `Bearer ${apiKey}`,
57
+ },
58
+ body: JSON.stringify({
59
+ data: reportData,
60
+ repoId, // Might be null, server will handle mapping
61
+ }),
62
+ });
63
+
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
+ }
73
+
74
+ if (!res.ok) {
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
+
95
+ if (res.status === 401) {
96
+ console.log(
97
+ chalk.dim(' Hint: Your API key may be invalid or expired.')
98
+ );
99
+ }
100
+ process.exit(1);
101
+ }
102
+
103
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
104
+ console.log(chalk.green(`\nāœ… Upload successful! (${duration}s)`));
105
+ console.log(chalk.cyan(` View results: ${serverUrl}/dashboard`));
106
+
107
+ if (result.analysis) {
108
+ console.log(chalk.dim(` Analysis ID: ${result.analysis.id}`));
109
+ console.log(chalk.dim(` Score: ${result.analysis.aiScore}/100`));
110
+ }
111
+ } catch (error) {
112
+ handleCLIError(error, 'Upload');
113
+ }
114
+ }
115
+
116
+ export const uploadHelpText = `
117
+ EXAMPLES:
118
+ $ aiready upload report.json --api-key ar_...
119
+ $ aiready upload .aiready/latest.json
120
+ $ AIREADY_API_KEY=ar_... aiready upload report.json
121
+
122
+ ENVIRONMENT VARIABLES:
123
+ AIREADY_API_KEY Your platform API key
124
+ AIREADY_SERVER Custom platform URL (default: https://dev.platform.getaiready.dev)
125
+ `;