@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/.turbo/turbo-build.log +8 -8
- package/README.md +22 -1
- package/dist/cli.js +404 -257
- package/dist/cli.mjs +364 -211
- package/package.json +12 -12
- package/src/cli.ts +18 -0
- package/src/commands/index.ts +1 -0
- package/src/commands/scan.ts +113 -7
- package/src/commands/upload.ts +87 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/cli",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
15
|
-
"@aiready/
|
|
16
|
-
"@aiready/
|
|
17
|
-
"@aiready/
|
|
18
|
-
"@aiready/
|
|
19
|
-
"@aiready/
|
|
20
|
-
"@aiready/
|
|
21
|
-
"@aiready/
|
|
22
|
-
"@aiready/
|
|
23
|
-
"@aiready/
|
|
24
|
-
"@aiready/
|
|
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();
|
package/src/commands/index.ts
CHANGED
|
@@ -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';
|
package/src/commands/scan.ts
CHANGED
|
@@ -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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 = {
|
|
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 = {
|
|
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
|
+
`;
|