@aiready/cli 0.9.43 → 0.9.45

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiready/cli",
3
- "version": "0.9.43",
3
+ "version": "0.9.45",
4
4
  "description": "Unified CLI for AIReady analysis tools",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -11,17 +11,17 @@
11
11
  "dependencies": {
12
12
  "chalk": "^5.3.0",
13
13
  "commander": "^14.0.0",
14
- "@aiready/core": "0.9.35",
15
- "@aiready/ai-signal-clarity": "0.1.8",
16
- "@aiready/agent-grounding": "0.1.8",
17
- "@aiready/pattern-detect": "0.11.34",
18
- "@aiready/testability": "0.1.8",
19
- "@aiready/context-analyzer": "0.9.38",
20
- "@aiready/visualizer": "0.1.40",
21
- "@aiready/consistency": "0.8.34",
22
- "@aiready/deps": "0.1.8",
23
- "@aiready/change-amplification": "0.1.8",
24
- "@aiready/doc-drift": "0.1.8"
14
+ "@aiready/core": "0.9.37",
15
+ "@aiready/deps": "0.1.10",
16
+ "@aiready/doc-drift": "0.1.10",
17
+ "@aiready/agent-grounding": "0.1.10",
18
+ "@aiready/ai-signal-clarity": "0.1.10",
19
+ "@aiready/visualizer": "0.1.42",
20
+ "@aiready/consistency": "0.8.36",
21
+ "@aiready/context-analyzer": "0.9.40",
22
+ "@aiready/testability": "0.1.10",
23
+ "@aiready/pattern-detect": "0.11.36",
24
+ "@aiready/change-amplification": "0.1.10"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@types/node": "^24.0.0",
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,9 +15,13 @@ 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
27
  import { analyzeUnified } from '../index';
@@ -26,6 +30,7 @@ import {
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 = {
@@ -74,11 +86,11 @@ export async function scanAction(directory: string, options: ScanOptions) {
74
86
 
75
87
  let profileTools = options.tools
76
88
  ? options.tools.split(',').map((t: string) => {
77
- const tool = t.trim();
78
- if (tool === 'hallucination' || tool === 'hallucination-risk')
79
- return 'aiSignalClarity';
80
- return tool;
81
- })
89
+ const tool = t.trim();
90
+ if (tool === 'hallucination' || tool === 'hallucination-risk')
91
+ return 'aiSignalClarity';
92
+ return tool;
93
+ })
82
94
  : undefined;
83
95
  if (options.profile) {
84
96
  switch (options.profile.toLowerCase()) {
@@ -489,6 +501,18 @@ export async function scanAction(directory: string, options: ScanOptions) {
489
501
  results.duplicates,
490
502
  results.patterns?.length || 0
491
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
+
492
516
  toolScores.set('pattern-detect', patternScore);
493
517
  } catch (err) {
494
518
  void err;
@@ -502,6 +526,17 @@ export async function scanAction(directory: string, options: ScanOptions) {
502
526
  try {
503
527
  const ctxSummary = genContextSummary(results.context);
504
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
+ });
539
+
505
540
  toolScores.set('context-analyzer', contextScore);
506
541
  } catch (err) {
507
542
  void err;
@@ -688,6 +723,49 @@ export async function scanAction(directory: string, options: ScanOptions) {
688
723
  }
689
724
  }
690
725
 
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
+ });
742
+
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
+ }
768
+
691
769
  // Show concise breakdown; detailed breakdown only if config requests it
692
770
  if (scoringResult.breakdown && scoringResult.breakdown.length > 0) {
693
771
  console.log(chalk.bold('\nTool breakdown:'));
@@ -723,13 +801,26 @@ export async function scanAction(directory: string, options: ScanOptions) {
723
801
  defaultFilename,
724
802
  resolvedDir
725
803
  );
726
- const outputData = { ...results, scoring: scoringResult };
804
+ const outputData = {
805
+ ...results,
806
+ scoring: scoringResult,
807
+ repository: repoMetadata,
808
+ };
727
809
  handleJSONOutput(
728
810
  outputData,
729
811
  outputPath,
730
812
  `āœ… Report saved to ${outputPath}`
731
813
  );
732
814
 
815
+ // Automatic Upload
816
+ if (options.upload) {
817
+ console.log(chalk.blue('\nšŸ“¤ Automatic upload triggered...'));
818
+ await uploadAction(outputPath, {
819
+ apiKey: options.apiKey,
820
+ server: options.server,
821
+ });
822
+ }
823
+
733
824
  // Warn if graph caps may be exceeded
734
825
  await warnIfGraphCapExceeded(outputData, resolvedDir);
735
826
  } else {
@@ -741,11 +832,24 @@ export async function scanAction(directory: string, options: ScanOptions) {
741
832
  defaultFilename,
742
833
  resolvedDir
743
834
  );
744
- const outputData = { ...results, scoring: scoringResult };
835
+ const outputData = {
836
+ ...results,
837
+ scoring: scoringResult,
838
+ repository: repoMetadata,
839
+ };
745
840
 
746
841
  try {
747
842
  writeFileSync(outputPath, JSON.stringify(outputData, null, 2));
748
843
  console.log(chalk.dim(`āœ… Report auto-persisted to ${outputPath}`));
844
+
845
+ // Automatic Upload (from auto-persistent report)
846
+ if (options.upload) {
847
+ console.log(chalk.blue('\nšŸ“¤ Automatic upload triggered...'));
848
+ await uploadAction(outputPath, {
849
+ apiKey: options.apiKey,
850
+ server: options.server,
851
+ });
852
+ }
749
853
  // Warn if graph caps may be exceeded
750
854
  await warnIfGraphCapExceeded(outputData, resolvedDir);
751
855
  } catch (err) {
@@ -897,6 +1001,8 @@ EXAMPLES:
897
1001
  $ aiready scan --ci --threshold 70 # GitHub Actions gatekeeper
898
1002
  $ aiready scan --ci --fail-on major # Fail on major+ issues
899
1003
  $ aiready scan --output json --output-file report.json
1004
+ $ aiready scan --upload --api-key ar_... # Automatic platform upload
1005
+ $ aiready scan --upload --server custom-url.com # Upload to custom platform
900
1006
 
901
1007
  PROFILES:
902
1008
  agentic: aiSignalClarity, grounding, testability
@@ -0,0 +1,87 @@
1
+ import fs from 'fs';
2
+ import path, { resolve as resolvePath } from 'path';
3
+ import chalk from 'chalk';
4
+ import {
5
+ handleCLIError,
6
+ } from '@aiready/core';
7
+
8
+ interface UploadOptions {
9
+ apiKey?: string;
10
+ repoId?: string;
11
+ server?: string;
12
+ }
13
+
14
+ export async function uploadAction(file: string, options: UploadOptions) {
15
+ const startTime = Date.now();
16
+ const filePath = resolvePath(process.cwd(), file);
17
+ const serverUrl = options.server || process.env.AIREADY_SERVER || 'https://dev.platform.getaiready.dev';
18
+ const apiKey = options.apiKey || process.env.AIREADY_API_KEY;
19
+
20
+ if (!apiKey) {
21
+ 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'));
24
+ process.exit(1);
25
+ }
26
+
27
+ if (!fs.existsSync(filePath)) {
28
+ console.error(chalk.red(`āŒ File not found: ${filePath}`));
29
+ process.exit(1);
30
+ }
31
+
32
+ try {
33
+ console.log(chalk.blue(`šŸš€ Uploading report to ${serverUrl}...`));
34
+
35
+ // Read the report file
36
+ const reportData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
37
+
38
+ // Prepare upload payload
39
+ // Note: repoId is optional if the metadata contains it, but for now we'll require it or infer from metadata
40
+ const repoId = options.repoId || reportData.repository?.repoId;
41
+
42
+ const res = await fetch(`${serverUrl}/api/analysis/upload`, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'Authorization': `Bearer ${apiKey}`,
47
+ },
48
+ body: JSON.stringify({
49
+ data: reportData,
50
+ repoId, // Might be null, server will handle mapping
51
+ }),
52
+ });
53
+
54
+ const result = (await res.json()) as any;
55
+
56
+ if (!res.ok) {
57
+ console.error(chalk.red(`āŒ Upload failed: ${result.error || res.statusText}`));
58
+ if (res.status === 401) {
59
+ console.log(chalk.dim(' Hint: Your API key may be invalid or expired.'));
60
+ }
61
+ process.exit(1);
62
+ }
63
+
64
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
65
+ console.log(chalk.green(`\nāœ… Upload successful! (${duration}s)`));
66
+ console.log(chalk.cyan(` View results: ${serverUrl}/dashboard`));
67
+
68
+ if (result.analysis) {
69
+ console.log(chalk.dim(` Analysis ID: ${result.analysis.id}`));
70
+ console.log(chalk.dim(` Score: ${result.analysis.aiScore}/100`));
71
+ }
72
+
73
+ } catch (error) {
74
+ handleCLIError(error, 'Upload');
75
+ }
76
+ }
77
+
78
+ export const uploadHelpText = `
79
+ EXAMPLES:
80
+ $ aiready upload report.json --api-key ar_...
81
+ $ aiready upload .aiready/latest.json
82
+ $ AIREADY_API_KEY=ar_... aiready upload report.json
83
+
84
+ ENVIRONMENT VARIABLES:
85
+ AIREADY_API_KEY Your platform API key
86
+ AIREADY_SERVER Custom platform URL (default: https://dev.platform.getaiready.dev)
87
+ `;