cloud-cost-cli 0.1.1 → 0.3.0-beta.1

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.
Files changed (46) hide show
  1. package/README.md +116 -122
  2. package/dist/bin/cloud-cost-cli.js +28 -3
  3. package/dist/src/analyzers/cost-estimator.d.ts +14 -0
  4. package/dist/src/analyzers/cost-estimator.js +74 -16
  5. package/dist/src/analyzers/pricing-service.d.ts +38 -0
  6. package/dist/src/analyzers/pricing-service.js +263 -0
  7. package/dist/src/commands/ask.d.ts +11 -0
  8. package/dist/src/commands/ask.js +164 -0
  9. package/dist/src/commands/config.d.ts +1 -0
  10. package/dist/src/commands/config.js +120 -0
  11. package/dist/src/commands/costs.d.ts +6 -0
  12. package/dist/src/commands/costs.js +54 -0
  13. package/dist/src/commands/scan.d.ts +6 -0
  14. package/dist/src/commands/scan.js +255 -85
  15. package/dist/src/commands/script.d.ts +8 -0
  16. package/dist/src/commands/script.js +27 -0
  17. package/dist/src/providers/azure/client.d.ts +20 -0
  18. package/dist/src/providers/azure/client.js +41 -0
  19. package/dist/src/providers/azure/disks.d.ts +4 -0
  20. package/dist/src/providers/azure/disks.js +87 -0
  21. package/dist/src/providers/azure/index.d.ts +6 -0
  22. package/dist/src/providers/azure/index.js +15 -0
  23. package/dist/src/providers/azure/public-ips.d.ts +3 -0
  24. package/dist/src/providers/azure/public-ips.js +47 -0
  25. package/dist/src/providers/azure/sql.d.ts +4 -0
  26. package/dist/src/providers/azure/sql.js +134 -0
  27. package/dist/src/providers/azure/storage.d.ts +8 -0
  28. package/dist/src/providers/azure/storage.js +100 -0
  29. package/dist/src/providers/azure/vms.d.ts +4 -0
  30. package/dist/src/providers/azure/vms.js +164 -0
  31. package/dist/src/reporters/table.d.ts +2 -1
  32. package/dist/src/reporters/table.js +69 -3
  33. package/dist/src/services/ai.d.ts +44 -0
  34. package/dist/src/services/ai.js +345 -0
  35. package/dist/src/services/script-generator.d.ts +21 -0
  36. package/dist/src/services/script-generator.js +245 -0
  37. package/dist/src/utils/cache.d.ts +25 -0
  38. package/dist/src/utils/cache.js +197 -0
  39. package/dist/src/utils/config.d.ts +37 -0
  40. package/dist/src/utils/config.js +175 -0
  41. package/dist/src/utils/cost-tracker.d.ts +33 -0
  42. package/dist/src/utils/cost-tracker.js +135 -0
  43. package/dist/src/utils/formatter.d.ts +2 -0
  44. package/dist/src/utils/formatter.js +29 -1
  45. package/docs/RELEASE.md +14 -25
  46. package/package.json +15 -3
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AZURE_SQL_PRICING = void 0;
4
+ exports.analyzeAzureSQL = analyzeAzureSQL;
5
+ // Azure SQL Database pricing (per month, East US, vCore model)
6
+ exports.AZURE_SQL_PRICING = {
7
+ 'GP_Gen5_2': 438.29, // General Purpose, 2 vCores
8
+ 'GP_Gen5_4': 876.58, // General Purpose, 4 vCores
9
+ 'GP_Gen5_8': 1753.16, // General Purpose, 8 vCores
10
+ 'BC_Gen5_2': 876.58, // Business Critical, 2 vCores
11
+ 'BC_Gen5_4': 1753.16, // Business Critical, 4 vCores
12
+ };
13
+ function getSQLMonthlyCost(sku) {
14
+ return exports.AZURE_SQL_PRICING[sku] || 500; // Fallback estimate
15
+ }
16
+ async function analyzeAzureSQL(client) {
17
+ const sqlClient = client.getSqlClient();
18
+ const monitorClient = client.getMonitorClient();
19
+ const opportunities = [];
20
+ try {
21
+ // List all SQL servers
22
+ const servers = sqlClient.servers.list();
23
+ for await (const server of servers) {
24
+ if (!server.id || !server.name)
25
+ continue;
26
+ // Filter by location if specified
27
+ if (client.location && server.location?.toLowerCase() !== client.location.toLowerCase()) {
28
+ continue;
29
+ }
30
+ const resourceGroup = extractResourceGroup(server.id);
31
+ if (!resourceGroup)
32
+ continue;
33
+ try {
34
+ // List databases on this server
35
+ const databases = sqlClient.databases.listByServer(resourceGroup, server.name);
36
+ for await (const db of databases) {
37
+ if (!db.id || !db.name || db.name === 'master')
38
+ continue;
39
+ const sku = db.sku?.name || 'Unknown';
40
+ const tier = db.sku?.tier || 'Unknown';
41
+ const currentCost = getSQLMonthlyCost(sku);
42
+ // Get DTU/CPU usage metrics
43
+ const avgDtu = await getAverageDTU(monitorClient, db.id, 7);
44
+ // Opportunity 1: Low utilization database
45
+ if (avgDtu < 10) {
46
+ opportunities.push({
47
+ id: `azure-sql-underutilized-${db.name}`,
48
+ provider: 'azure',
49
+ resourceType: 'sql',
50
+ resourceId: db.id,
51
+ resourceName: db.name,
52
+ category: 'oversized',
53
+ currentCost,
54
+ estimatedSavings: currentCost * 0.5, // Could downsize by ~50%
55
+ confidence: 'medium',
56
+ recommendation: `Database is underutilized (${avgDtu.toFixed(1)}% avg DTU). Consider downsizing tier.`,
57
+ metadata: {
58
+ sku,
59
+ tier,
60
+ avgDtu,
61
+ server: server.name,
62
+ location: db.location,
63
+ },
64
+ detectedAt: new Date(),
65
+ });
66
+ }
67
+ // Opportunity 2: Business Critical that could be General Purpose
68
+ if (tier === 'BusinessCritical' && avgDtu < 30) {
69
+ opportunities.push({
70
+ id: `azure-sql-tier-${db.name}`,
71
+ provider: 'azure',
72
+ resourceType: 'sql',
73
+ resourceId: db.id,
74
+ resourceName: db.name,
75
+ category: 'oversized',
76
+ currentCost,
77
+ estimatedSavings: currentCost * 0.5, // BC is ~2x GP cost
78
+ confidence: 'low',
79
+ recommendation: 'Consider switching from Business Critical to General Purpose tier if high availability is not required.',
80
+ metadata: {
81
+ sku,
82
+ tier,
83
+ avgDtu,
84
+ server: server.name,
85
+ },
86
+ detectedAt: new Date(),
87
+ });
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ // Skip this server if we can't fetch databases
93
+ continue;
94
+ }
95
+ }
96
+ return opportunities;
97
+ }
98
+ catch (error) {
99
+ console.error('Error analyzing Azure SQL:', error);
100
+ return opportunities;
101
+ }
102
+ }
103
+ async function getAverageDTU(monitorClient, resourceId, days) {
104
+ try {
105
+ const endTime = new Date();
106
+ const startTime = new Date();
107
+ startTime.setDate(startTime.getDate() - days);
108
+ const metrics = await monitorClient.metrics.list(resourceId, {
109
+ timespan: `${startTime.toISOString()}/${endTime.toISOString()}`,
110
+ interval: 'PT1H',
111
+ metricnames: 'dtu_consumption_percent',
112
+ aggregation: 'Average',
113
+ });
114
+ const timeseries = metrics.value?.[0]?.timeseries?.[0]?.data || [];
115
+ if (timeseries.length === 0) {
116
+ return 0;
117
+ }
118
+ const values = timeseries
119
+ .map((d) => d.average)
120
+ .filter((v) => v !== null && v !== undefined);
121
+ if (values.length === 0) {
122
+ return 0;
123
+ }
124
+ const sum = values.reduce((a, b) => a + b, 0);
125
+ return sum / values.length;
126
+ }
127
+ catch (error) {
128
+ return 0;
129
+ }
130
+ }
131
+ function extractResourceGroup(resourceId) {
132
+ const match = resourceId.match(/resourceGroups\/([^\/]+)/i);
133
+ return match ? match[1] : null;
134
+ }
@@ -0,0 +1,8 @@
1
+ import { AzureClient } from './client';
2
+ import { SavingsOpportunity } from '../../types/opportunity';
3
+ export declare const AZURE_STORAGE_PRICING: {
4
+ hot: number;
5
+ cool: number;
6
+ archive: number;
7
+ };
8
+ export declare function analyzeAzureStorage(client: AzureClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AZURE_STORAGE_PRICING = void 0;
4
+ exports.analyzeAzureStorage = analyzeAzureStorage;
5
+ // Azure Blob Storage pricing (per GB/month, East US)
6
+ exports.AZURE_STORAGE_PRICING = {
7
+ hot: 0.0184,
8
+ cool: 0.01,
9
+ archive: 0.002,
10
+ };
11
+ async function analyzeAzureStorage(client) {
12
+ const storageClient = client.getStorageClient();
13
+ const opportunities = [];
14
+ try {
15
+ // List all storage accounts
16
+ const accounts = storageClient.storageAccounts.list();
17
+ for await (const account of accounts) {
18
+ if (!account.id || !account.name)
19
+ continue;
20
+ // Filter by location if specified
21
+ if (client.location && account.location?.toLowerCase() !== client.location.toLowerCase()) {
22
+ continue;
23
+ }
24
+ const resourceGroup = extractResourceGroup(account.id);
25
+ if (!resourceGroup)
26
+ continue;
27
+ try {
28
+ // Get blob service properties
29
+ const blobServices = await storageClient.blobServices.getServiceProperties(resourceGroup, account.name);
30
+ // Check if lifecycle management is enabled
31
+ const hasLifecyclePolicy = await hasLifecycleManagement(storageClient, resourceGroup, account.name);
32
+ if (!hasLifecyclePolicy) {
33
+ // Estimate potential savings (assume 30% of data could move to cool/archive)
34
+ // This is a rough estimate - actual savings depend on usage patterns
35
+ const estimatedSavings = 50; // Conservative estimate
36
+ opportunities.push({
37
+ id: `azure-storage-lifecycle-${account.name}`,
38
+ provider: 'azure',
39
+ resourceType: 'storage',
40
+ resourceId: account.id,
41
+ resourceName: account.name,
42
+ category: 'misconfigured',
43
+ currentCost: 100, // Placeholder - hard to estimate without usage data
44
+ estimatedSavings,
45
+ confidence: 'low',
46
+ recommendation: 'Enable lifecycle management to automatically move old data to Cool or Archive tiers.',
47
+ metadata: {
48
+ location: account.location,
49
+ accountType: account.kind,
50
+ replication: account.sku?.name,
51
+ },
52
+ detectedAt: new Date(),
53
+ });
54
+ }
55
+ // Check for public access
56
+ if (account.allowBlobPublicAccess === true) {
57
+ opportunities.push({
58
+ id: `azure-storage-public-${account.name}`,
59
+ provider: 'azure',
60
+ resourceType: 'storage',
61
+ resourceId: account.id,
62
+ resourceName: account.name,
63
+ category: 'misconfigured',
64
+ currentCost: 0,
65
+ estimatedSavings: 0,
66
+ confidence: 'high',
67
+ recommendation: 'Public blob access is enabled. Review security settings.',
68
+ metadata: {
69
+ location: account.location,
70
+ issue: 'security',
71
+ },
72
+ detectedAt: new Date(),
73
+ });
74
+ }
75
+ }
76
+ catch (error) {
77
+ // Skip this account if we can't fetch details
78
+ continue;
79
+ }
80
+ }
81
+ return opportunities;
82
+ }
83
+ catch (error) {
84
+ console.error('Error analyzing Azure storage:', error);
85
+ return opportunities;
86
+ }
87
+ }
88
+ async function hasLifecycleManagement(storageClient, resourceGroup, accountName) {
89
+ try {
90
+ const policy = await storageClient.managementPolicies.get(resourceGroup, accountName, 'default');
91
+ return policy && policy.policy && policy.policy.rules && policy.policy.rules.length > 0;
92
+ }
93
+ catch (error) {
94
+ return false;
95
+ }
96
+ }
97
+ function extractResourceGroup(resourceId) {
98
+ const match = resourceId.match(/resourceGroups\/([^\/]+)/i);
99
+ return match ? match[1] : null;
100
+ }
@@ -0,0 +1,4 @@
1
+ import { AzureClient } from './client';
2
+ import { SavingsOpportunity } from '../../types/opportunity';
3
+ export declare const AZURE_VM_PRICING: Record<string, number>;
4
+ export declare function analyzeAzureVMs(client: AzureClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AZURE_VM_PRICING = void 0;
7
+ exports.analyzeAzureVMs = analyzeAzureVMs;
8
+ const dayjs_1 = __importDefault(require("dayjs"));
9
+ // Azure VM pricing (East US, pay-as-you-go, monthly estimate)
10
+ // Source: Azure pricing as of 2026-01, monthly = hourly Ɨ 730
11
+ exports.AZURE_VM_PRICING = {
12
+ 'Standard_B1s': 7.59,
13
+ 'Standard_B1ms': 15.33,
14
+ 'Standard_B2s': 30.37,
15
+ 'Standard_B2ms': 60.74,
16
+ 'Standard_D2s_v3': 70.08,
17
+ 'Standard_D4s_v3': 140.16,
18
+ 'Standard_D8s_v3': 280.32,
19
+ 'Standard_E2s_v3': 109.50,
20
+ 'Standard_E4s_v3': 219.00,
21
+ 'Standard_F2s_v2': 62.05,
22
+ 'Standard_F4s_v2': 124.10,
23
+ };
24
+ function getVMMonthlyCost(vmSize) {
25
+ return exports.AZURE_VM_PRICING[vmSize] || 100; // Fallback estimate
26
+ }
27
+ async function analyzeAzureVMs(client) {
28
+ const computeClient = client.getComputeClient();
29
+ const monitorClient = client.getMonitorClient();
30
+ const opportunities = [];
31
+ try {
32
+ // List all VMs in the subscription
33
+ const vms = computeClient.virtualMachines.listAll();
34
+ for await (const vm of vms) {
35
+ if (!vm.id || !vm.name)
36
+ continue;
37
+ // Filter by location if specified
38
+ if (client.location && vm.location?.toLowerCase() !== client.location.toLowerCase()) {
39
+ continue;
40
+ }
41
+ const vmSize = vm.hardwareProfile?.vmSize || 'Unknown';
42
+ const powerState = await getVMPowerState(computeClient, vm);
43
+ // Skip stopped/deallocated VMs (no compute cost)
44
+ if (powerState === 'stopped' || powerState === 'deallocated') {
45
+ continue;
46
+ }
47
+ const currentCost = getVMMonthlyCost(vmSize);
48
+ // Get CPU metrics for the last 7 days
49
+ const avgCpu = await getAverageCPU(monitorClient, vm.id, 7);
50
+ // Opportunity 1: Idle VM (low CPU usage)
51
+ if (avgCpu < 5) {
52
+ opportunities.push({
53
+ id: `azure-vm-idle-${vm.name}`,
54
+ provider: 'azure',
55
+ resourceType: 'vm',
56
+ resourceId: vm.id,
57
+ resourceName: vm.name,
58
+ category: 'idle',
59
+ currentCost,
60
+ estimatedSavings: currentCost * 0.9, // Could stop or downsize
61
+ confidence: 'high',
62
+ recommendation: `VM is idle (${avgCpu.toFixed(1)}% avg CPU). Consider stopping or downsizing.`,
63
+ metadata: {
64
+ vmSize,
65
+ avgCpu,
66
+ location: vm.location,
67
+ powerState,
68
+ },
69
+ detectedAt: new Date(),
70
+ });
71
+ }
72
+ // Opportunity 2: Underutilized VM (medium CPU usage)
73
+ else if (avgCpu < 20) {
74
+ const smallerSize = getSmallerVMSize(vmSize);
75
+ if (smallerSize) {
76
+ const newCost = getVMMonthlyCost(smallerSize);
77
+ const savings = currentCost - newCost;
78
+ if (savings > 10) { // At least $10/month savings
79
+ opportunities.push({
80
+ id: `azure-vm-underutilized-${vm.name}`,
81
+ provider: 'azure',
82
+ resourceType: 'vm',
83
+ resourceId: vm.id,
84
+ resourceName: vm.name,
85
+ category: 'oversized',
86
+ currentCost,
87
+ estimatedSavings: savings,
88
+ confidence: 'medium',
89
+ recommendation: `Downsize from ${vmSize} to ${smallerSize} (${avgCpu.toFixed(1)}% avg CPU).`,
90
+ metadata: {
91
+ vmSize,
92
+ suggestedSize: smallerSize,
93
+ avgCpu,
94
+ location: vm.location,
95
+ },
96
+ detectedAt: new Date(),
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return opportunities;
103
+ }
104
+ catch (error) {
105
+ console.error('Error analyzing Azure VMs:', error);
106
+ return opportunities;
107
+ }
108
+ }
109
+ async function getVMPowerState(computeClient, vm) {
110
+ try {
111
+ const resourceGroup = extractResourceGroup(vm.id);
112
+ if (!resourceGroup || !vm.name)
113
+ return 'unknown';
114
+ const instanceView = await computeClient.virtualMachines.instanceView(resourceGroup, vm.name);
115
+ const powerState = instanceView.statuses?.find((s) => s.code?.startsWith('PowerState/'));
116
+ return powerState?.code?.replace('PowerState/', '').toLowerCase() || 'unknown';
117
+ }
118
+ catch (error) {
119
+ return 'unknown';
120
+ }
121
+ }
122
+ async function getAverageCPU(monitorClient, resourceId, days) {
123
+ try {
124
+ const endTime = new Date();
125
+ const startTime = (0, dayjs_1.default)().subtract(days, 'days').toDate();
126
+ const metrics = await monitorClient.metrics.list(resourceId, {
127
+ timespan: `${startTime.toISOString()}/${endTime.toISOString()}`,
128
+ interval: 'PT1H',
129
+ metricnames: 'Percentage CPU',
130
+ aggregation: 'Average',
131
+ });
132
+ const timeseries = metrics.value?.[0]?.timeseries?.[0]?.data || [];
133
+ if (timeseries.length === 0) {
134
+ return 0;
135
+ }
136
+ const values = timeseries
137
+ .map((d) => d.average)
138
+ .filter((v) => v !== null && v !== undefined);
139
+ if (values.length === 0) {
140
+ return 0;
141
+ }
142
+ const sum = values.reduce((a, b) => a + b, 0);
143
+ return sum / values.length;
144
+ }
145
+ catch (error) {
146
+ console.error('Error fetching CPU metrics:', error);
147
+ return 0;
148
+ }
149
+ }
150
+ function getSmallerVMSize(currentSize) {
151
+ const downsizeMap = {
152
+ 'Standard_B2ms': 'Standard_B1ms',
153
+ 'Standard_B2s': 'Standard_B1s',
154
+ 'Standard_D4s_v3': 'Standard_D2s_v3',
155
+ 'Standard_D8s_v3': 'Standard_D4s_v3',
156
+ 'Standard_E4s_v3': 'Standard_E2s_v3',
157
+ 'Standard_F4s_v2': 'Standard_F2s_v2',
158
+ };
159
+ return downsizeMap[currentSize] || null;
160
+ }
161
+ function extractResourceGroup(resourceId) {
162
+ const match = resourceId.match(/resourceGroups\/([^\/]+)/i);
163
+ return match ? match[1] : null;
164
+ }
@@ -1,2 +1,3 @@
1
1
  import { ScanReport } from '../types';
2
- export declare function renderTable(report: ScanReport, topN?: number): void;
2
+ import { AIService } from '../services/ai';
3
+ export declare function renderTable(report: ScanReport, topN?: number, aiService?: AIService): Promise<void>;
@@ -7,7 +7,7 @@ exports.renderTable = renderTable;
7
7
  const cli_table3_1 = __importDefault(require("cli-table3"));
8
8
  const chalk_1 = __importDefault(require("chalk"));
9
9
  const utils_1 = require("../utils");
10
- function renderTable(report, topN = 5) {
10
+ async function renderTable(report, topN = 5, aiService) {
11
11
  console.log(chalk_1.default.bold('\nCloud Cost Optimization Report'));
12
12
  console.log(`Provider: ${report.provider} | Region: ${report.region} | Account: ${report.accountId}`);
13
13
  console.log(`Analyzed: ${report.scanPeriod.start.toISOString().split('T')[0]} to ${report.scanPeriod.end.toISOString().split('T')[0]}\n`);
@@ -21,7 +21,8 @@ function renderTable(report, topN = 5) {
21
21
  console.log(chalk_1.default.bold(`Top ${opportunities.length} Savings Opportunities (est. ${(0, utils_1.formatCurrency)(report.totalPotentialSavings)}/month):\n`));
22
22
  const table = new cli_table3_1.default({
23
23
  head: ['#', 'Type', 'Resource ID', 'Recommendation', 'Savings/mo'],
24
- colWidths: [5, 10, 25, 50, 15],
24
+ colWidths: [5, 12, 40, 60, 15],
25
+ wordWrap: true,
25
26
  style: {
26
27
  head: ['cyan'],
27
28
  },
@@ -36,6 +37,71 @@ function renderTable(report, topN = 5) {
36
37
  ]);
37
38
  });
38
39
  console.log(table.toString());
40
+ // Show AI explanations if enabled
41
+ if (aiService && aiService.isEnabled()) {
42
+ console.log(chalk_1.default.bold('\nšŸ¤– AI-Powered Insights:\n'));
43
+ const maxExplanations = aiService.getMaxExplanations();
44
+ const opportunitiesToExplain = opportunities.slice(0, Math.min(maxExplanations, opportunities.length));
45
+ for (let i = 0; i < opportunitiesToExplain.length; i++) {
46
+ const opp = opportunitiesToExplain[i];
47
+ try {
48
+ console.log(chalk_1.default.cyan(`Analyzing opportunity #${i + 1}...`));
49
+ const explanation = await aiService.explainOpportunity(opp);
50
+ const cacheIndicator = explanation.cached ? chalk_1.default.dim(' (cached)') : '';
51
+ console.log(chalk_1.default.bold(`\nšŸ’” Opportunity #${i + 1}: ${opp.resourceId}${cacheIndicator}`));
52
+ console.log(chalk_1.default.dim('─'.repeat(80)));
53
+ console.log(chalk_1.default.white(explanation.summary));
54
+ console.log();
55
+ console.log(chalk_1.default.bold('Why this is wasteful:'));
56
+ console.log(explanation.whyWasteful);
57
+ if (explanation.actionPlan.length > 0) {
58
+ console.log();
59
+ console.log(chalk_1.default.bold('Action plan:'));
60
+ explanation.actionPlan.forEach((step) => {
61
+ console.log(chalk_1.default.green(` ${step}`));
62
+ });
63
+ }
64
+ console.log();
65
+ console.log(`Risk: ${getRiskEmoji(explanation.riskLevel)} ${explanation.riskLevel.toUpperCase()}`);
66
+ console.log(`Time: ā±ļø ${explanation.estimatedTime}`);
67
+ // Try to generate remediation script
68
+ try {
69
+ const script = await aiService.generateRemediationScript(opp);
70
+ if (script) {
71
+ console.log();
72
+ console.log(chalk_1.default.bold('šŸ”§ Remediation Script:'));
73
+ console.log(chalk_1.default.dim('─'.repeat(80)));
74
+ console.log(chalk_1.default.gray(script));
75
+ }
76
+ }
77
+ catch (error) {
78
+ // Script generation failed, skip silently
79
+ }
80
+ console.log();
81
+ }
82
+ catch (error) {
83
+ console.log(chalk_1.default.yellow(`āš ļø AI explanation failed: ${error.message}`));
84
+ }
85
+ }
86
+ }
87
+ // Show total count if there are more opportunities
88
+ if (report.opportunities.length > topN) {
89
+ console.log(chalk_1.default.dim(`\n... and ${report.opportunities.length - topN} more opportunities (use --top ${report.opportunities.length} to see all)`));
90
+ }
39
91
  console.log(chalk_1.default.bold(`\nTotal potential savings: ${chalk_1.default.green((0, utils_1.formatCurrency)(report.totalPotentialSavings))}/month (${chalk_1.default.green((0, utils_1.formatCurrency)(report.totalPotentialSavings * 12))}/year)`));
40
- console.log(`\nSummary: ${report.summary.totalResources} resources analyzed | ${report.summary.idleResources} idle | ${report.summary.oversizedResources} oversized | ${report.summary.unusedResources} unused\n`);
92
+ console.log(`\nSummary: ${report.summary.totalResources} resources analyzed | ${report.summary.idleResources} idle | ${report.summary.oversizedResources} oversized | ${report.summary.unusedResources} unused`);
93
+ console.log(chalk_1.default.dim(`\nšŸ’” Note: Cost estimates based on us-east-1 pricing and may vary by region.`));
94
+ console.log(chalk_1.default.dim(` For more accurate estimates, actual costs depend on your usage and region.\n`));
95
+ }
96
+ function getRiskEmoji(risk) {
97
+ switch (risk) {
98
+ case 'low':
99
+ return 'āœ…';
100
+ case 'medium':
101
+ return 'āš ļø';
102
+ case 'high':
103
+ return '🚨';
104
+ default:
105
+ return 'ā“';
106
+ }
41
107
  }
@@ -0,0 +1,44 @@
1
+ import { SavingsOpportunity } from '../types';
2
+ export interface AIExplanation {
3
+ summary: string;
4
+ whyWasteful: string;
5
+ actionPlan: string[];
6
+ riskLevel: 'low' | 'medium' | 'high';
7
+ estimatedTime: string;
8
+ script?: string;
9
+ cached?: boolean;
10
+ }
11
+ export interface QueryAnswer {
12
+ response: string;
13
+ suggestions?: string[];
14
+ relatedOpportunities?: any[];
15
+ }
16
+ export type AIProvider = 'openai' | 'ollama';
17
+ export interface AIConfig {
18
+ provider: AIProvider;
19
+ apiKey?: string;
20
+ model?: string;
21
+ maxExplanations?: number;
22
+ }
23
+ export declare class AIService {
24
+ private openaiClient;
25
+ private ollamaClient;
26
+ private provider;
27
+ private model;
28
+ private enabled;
29
+ private maxExplanations;
30
+ private cache;
31
+ private useCache;
32
+ private costTracker;
33
+ constructor(config?: AIConfig);
34
+ isEnabled(): boolean;
35
+ getMaxExplanations(): number;
36
+ explainOpportunity(opportunity: SavingsOpportunity): Promise<AIExplanation>;
37
+ generateRemediationScript(opportunity: SavingsOpportunity): Promise<string | null>;
38
+ private buildPrompt;
39
+ private parseExplanation;
40
+ answerQuery(query: string, scanReport: any): Promise<QueryAnswer>;
41
+ private buildQueryContext;
42
+ private buildQueryPrompt;
43
+ private parseQueryAnswer;
44
+ }