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,263 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PricingService = void 0;
4
+ exports.getS3MonthlyCost = getS3MonthlyCost;
5
+ exports.getELBMonthlyCost = getELBMonthlyCost;
6
+ exports.getEIPMonthlyCost = getEIPMonthlyCost;
7
+ const client_pricing_1 = require("@aws-sdk/client-pricing");
8
+ // Cache for pricing data to avoid repeated API calls
9
+ const pricingCache = new Map();
10
+ class PricingService {
11
+ pricingClient;
12
+ region;
13
+ constructor(region = 'us-east-1') {
14
+ this.region = region;
15
+ // Pricing API is only available in us-east-1 and ap-south-1
16
+ this.pricingClient = new client_pricing_1.PricingClient({ region: 'us-east-1' });
17
+ }
18
+ /**
19
+ * Get EC2 instance on-demand pricing for the configured region
20
+ */
21
+ async getEC2Price(instanceType) {
22
+ const cacheKey = `ec2-${this.region}-${instanceType}`;
23
+ if (pricingCache.has(cacheKey)) {
24
+ return pricingCache.get(cacheKey);
25
+ }
26
+ try {
27
+ const response = await this.pricingClient.send(new client_pricing_1.GetProductsCommand({
28
+ ServiceCode: 'AmazonEC2',
29
+ Filters: [
30
+ {
31
+ Type: 'TERM_MATCH',
32
+ Field: 'instanceType',
33
+ Value: instanceType,
34
+ },
35
+ {
36
+ Type: 'TERM_MATCH',
37
+ Field: 'location',
38
+ Value: this.getLocationName(this.region),
39
+ },
40
+ {
41
+ Type: 'TERM_MATCH',
42
+ Field: 'tenancy',
43
+ Value: 'Shared',
44
+ },
45
+ {
46
+ Type: 'TERM_MATCH',
47
+ Field: 'operatingSystem',
48
+ Value: 'Linux',
49
+ },
50
+ {
51
+ Type: 'TERM_MATCH',
52
+ Field: 'preInstalledSw',
53
+ Value: 'NA',
54
+ },
55
+ {
56
+ Type: 'TERM_MATCH',
57
+ Field: 'capacitystatus',
58
+ Value: 'Used',
59
+ },
60
+ ],
61
+ MaxResults: 1,
62
+ }));
63
+ const hourlyPrice = this.extractPriceFromResponse(response.PriceList);
64
+ const monthlyPrice = hourlyPrice * 730; // 730 hours per month
65
+ pricingCache.set(cacheKey, monthlyPrice);
66
+ return monthlyPrice;
67
+ }
68
+ catch (error) {
69
+ // Fallback to estimate if API fails
70
+ return this.getFallbackEC2Price(instanceType);
71
+ }
72
+ }
73
+ /**
74
+ * Get EBS volume pricing per GB
75
+ */
76
+ async getEBSPrice(volumeType) {
77
+ const cacheKey = `ebs-${this.region}-${volumeType}`;
78
+ if (pricingCache.has(cacheKey)) {
79
+ return pricingCache.get(cacheKey);
80
+ }
81
+ try {
82
+ const response = await this.pricingClient.send(new client_pricing_1.GetProductsCommand({
83
+ ServiceCode: 'AmazonEC2',
84
+ Filters: [
85
+ {
86
+ Type: 'TERM_MATCH',
87
+ Field: 'productFamily',
88
+ Value: 'Storage',
89
+ },
90
+ {
91
+ Type: 'TERM_MATCH',
92
+ Field: 'volumeApiName',
93
+ Value: volumeType,
94
+ },
95
+ {
96
+ Type: 'TERM_MATCH',
97
+ Field: 'location',
98
+ Value: this.getLocationName(this.region),
99
+ },
100
+ ],
101
+ MaxResults: 1,
102
+ }));
103
+ const pricePerGB = this.extractPriceFromResponse(response.PriceList);
104
+ pricingCache.set(cacheKey, pricePerGB);
105
+ return pricePerGB;
106
+ }
107
+ catch (error) {
108
+ return this.getFallbackEBSPrice(volumeType);
109
+ }
110
+ }
111
+ /**
112
+ * Get RDS instance pricing
113
+ */
114
+ async getRDSPrice(instanceClass, engine = 'mysql') {
115
+ const cacheKey = `rds-${this.region}-${instanceClass}-${engine}`;
116
+ if (pricingCache.has(cacheKey)) {
117
+ return pricingCache.get(cacheKey);
118
+ }
119
+ try {
120
+ const response = await this.pricingClient.send(new client_pricing_1.GetProductsCommand({
121
+ ServiceCode: 'AmazonRDS',
122
+ Filters: [
123
+ {
124
+ Type: 'TERM_MATCH',
125
+ Field: 'instanceType',
126
+ Value: instanceClass,
127
+ },
128
+ {
129
+ Type: 'TERM_MATCH',
130
+ Field: 'location',
131
+ Value: this.getLocationName(this.region),
132
+ },
133
+ {
134
+ Type: 'TERM_MATCH',
135
+ Field: 'databaseEngine',
136
+ Value: this.normalizeEngine(engine),
137
+ },
138
+ {
139
+ Type: 'TERM_MATCH',
140
+ Field: 'deploymentOption',
141
+ Value: 'Single-AZ',
142
+ },
143
+ ],
144
+ MaxResults: 1,
145
+ }));
146
+ const hourlyPrice = this.extractPriceFromResponse(response.PriceList);
147
+ const monthlyPrice = hourlyPrice * 730;
148
+ pricingCache.set(cacheKey, monthlyPrice);
149
+ return monthlyPrice;
150
+ }
151
+ catch (error) {
152
+ return this.getFallbackRDSPrice(instanceClass);
153
+ }
154
+ }
155
+ /**
156
+ * Extract price from AWS Pricing API response
157
+ */
158
+ extractPriceFromResponse(priceList) {
159
+ if (!priceList || priceList.length === 0) {
160
+ return 0;
161
+ }
162
+ try {
163
+ const product = JSON.parse(priceList[0]);
164
+ const terms = product.terms?.OnDemand;
165
+ if (!terms)
166
+ return 0;
167
+ const termKey = Object.keys(terms)[0];
168
+ const priceDimensions = terms[termKey]?.priceDimensions;
169
+ if (!priceDimensions)
170
+ return 0;
171
+ const dimensionKey = Object.keys(priceDimensions)[0];
172
+ const pricePerUnit = priceDimensions[dimensionKey]?.pricePerUnit?.USD;
173
+ return parseFloat(pricePerUnit || '0');
174
+ }
175
+ catch (error) {
176
+ return 0;
177
+ }
178
+ }
179
+ /**
180
+ * Convert AWS region code to location name used in Pricing API
181
+ */
182
+ getLocationName(region) {
183
+ const locationMap = {
184
+ 'us-east-1': 'US East (N. Virginia)',
185
+ 'us-east-2': 'US East (Ohio)',
186
+ 'us-west-1': 'US West (N. California)',
187
+ 'us-west-2': 'US West (Oregon)',
188
+ 'eu-west-1': 'EU (Ireland)',
189
+ 'eu-central-1': 'EU (Frankfurt)',
190
+ 'ap-southeast-1': 'Asia Pacific (Singapore)',
191
+ 'ap-southeast-2': 'Asia Pacific (Sydney)',
192
+ 'ap-northeast-1': 'Asia Pacific (Tokyo)',
193
+ // Add more as needed
194
+ };
195
+ return locationMap[region] || 'US East (N. Virginia)';
196
+ }
197
+ /**
198
+ * Normalize RDS engine name for Pricing API
199
+ */
200
+ normalizeEngine(engine) {
201
+ const engineMap = {
202
+ 'mysql': 'MySQL',
203
+ 'postgres': 'PostgreSQL',
204
+ 'mariadb': 'MariaDB',
205
+ 'aurora': 'Aurora MySQL',
206
+ 'aurora-mysql': 'Aurora MySQL',
207
+ 'aurora-postgresql': 'Aurora PostgreSQL',
208
+ };
209
+ return engineMap[engine.toLowerCase()] || 'MySQL';
210
+ }
211
+ /**
212
+ * Fallback estimates when API calls fail (based on us-east-1)
213
+ */
214
+ getFallbackEC2Price(instanceType) {
215
+ const estimates = {
216
+ 't3.micro': 7.59,
217
+ 't3.small': 15.18,
218
+ 't3.medium': 30.37,
219
+ 't3.large': 60.74,
220
+ 'm5.large': 70.08,
221
+ 'm5.xlarge': 140.16,
222
+ };
223
+ return estimates[instanceType] || 50; // Generic estimate
224
+ }
225
+ getFallbackEBSPrice(volumeType) {
226
+ const estimates = {
227
+ 'gp3': 0.08,
228
+ 'gp2': 0.10,
229
+ 'io1': 0.125,
230
+ 'io2': 0.125,
231
+ };
232
+ return estimates[volumeType] || 0.08;
233
+ }
234
+ getFallbackRDSPrice(instanceClass) {
235
+ const estimates = {
236
+ 'db.t3.micro': 11.01,
237
+ 'db.t3.small': 22.63,
238
+ 'db.t3.medium': 45.26,
239
+ 'db.t3.large': 90.51,
240
+ };
241
+ return estimates[instanceClass] || 100;
242
+ }
243
+ }
244
+ exports.PricingService = PricingService;
245
+ // Simple estimates for services where Pricing API is complex (S3, ELB, EIP)
246
+ function getS3MonthlyCost(sizeGB, storageClass = 'standard') {
247
+ const pricing = {
248
+ 'STANDARD': 0.023,
249
+ 'INTELLIGENT_TIERING': 0.023,
250
+ 'GLACIER': 0.004,
251
+ 'DEEP_ARCHIVE': 0.00099,
252
+ };
253
+ const pricePerGB = pricing[storageClass] || 0.023;
254
+ return sizeGB * pricePerGB;
255
+ }
256
+ function getELBMonthlyCost(type = 'alb') {
257
+ // ELB pricing is relatively consistent across regions (~$16-18/month base)
258
+ return type === 'clb' ? 18.25 : 16.43;
259
+ }
260
+ function getEIPMonthlyCost() {
261
+ // ~$3.65/month when unattached (consistent across regions)
262
+ return 3.65;
263
+ }
@@ -0,0 +1,11 @@
1
+ interface AskCommandOptions {
2
+ provider?: string;
3
+ aiProvider?: string;
4
+ aiModel?: string;
5
+ region?: string;
6
+ location?: string;
7
+ subscriptionId?: string;
8
+ }
9
+ export declare function askCommand(query: string, options: AskCommandOptions): Promise<void>;
10
+ export declare function saveScanCache(provider: string, region: string | undefined, report: any): void;
11
+ export {};
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.askCommand = askCommand;
40
+ exports.saveScanCache = saveScanCache;
41
+ const ai_1 = require("../services/ai");
42
+ const logger_1 = require("../utils/logger");
43
+ const config_1 = require("../utils/config");
44
+ const chalk_1 = __importDefault(require("chalk"));
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
47
+ async function askCommand(query, options) {
48
+ (0, logger_1.info)(`Analyzing: "${query}"`);
49
+ // Load most recent scan results
50
+ const scanData = loadRecentScan();
51
+ if (!scanData) {
52
+ (0, logger_1.error)('No recent scan found. Please run a scan first:');
53
+ (0, logger_1.info)(' cloud-cost-cli scan --provider aws --region us-east-1');
54
+ process.exit(1);
55
+ }
56
+ const ageMinutes = Math.floor((Date.now() - scanData.timestamp) / 60000);
57
+ (0, logger_1.info)(`Using scan from ${ageMinutes} minutes ago (${scanData.provider}${scanData.region ? ', ' + scanData.region : ''})`);
58
+ // Initialize AI service
59
+ // Load config file to get defaults
60
+ const fileConfig = config_1.ConfigLoader.load();
61
+ // CLI flags override config file
62
+ const provider = options.aiProvider || fileConfig.ai?.provider || 'openai';
63
+ const model = options.aiModel || fileConfig.ai?.model;
64
+ if (provider === 'openai' && !process.env.OPENAI_API_KEY && !fileConfig.ai?.apiKey) {
65
+ (0, logger_1.error)('Natural language queries require OPENAI_API_KEY or --ai-provider ollama');
66
+ (0, logger_1.info)('Set it with: export OPENAI_API_KEY="sk-..."');
67
+ (0, logger_1.info)('Or run: cloud-cost-cli config set ai.provider ollama');
68
+ process.exit(1);
69
+ }
70
+ try {
71
+ const aiService = new ai_1.AIService({
72
+ provider,
73
+ apiKey: provider === 'openai' ? (process.env.OPENAI_API_KEY || fileConfig.ai?.apiKey) : undefined,
74
+ model,
75
+ });
76
+ console.log(chalk_1.default.cyan('\n🤔 Thinking...\n'));
77
+ const answer = await aiService.answerQuery(query, scanData.report);
78
+ console.log(chalk_1.default.bold('💡 Answer:\n'));
79
+ console.log(chalk_1.default.white(answer.response));
80
+ if (answer.suggestions && answer.suggestions.length > 0) {
81
+ console.log(chalk_1.default.bold('\n📋 Suggestions:\n'));
82
+ answer.suggestions.forEach((suggestion, i) => {
83
+ console.log(chalk_1.default.green(` ${i + 1}. ${suggestion}`));
84
+ });
85
+ }
86
+ if (answer.relatedOpportunities && answer.relatedOpportunities.length > 0) {
87
+ console.log(chalk_1.default.bold('\n🎯 Related Opportunities:\n'));
88
+ answer.relatedOpportunities.forEach((opp, i) => {
89
+ console.log(chalk_1.default.yellow(` ${i + 1}. ${opp.resourceId}: ${opp.recommendation} (Save $${opp.estimatedSavings.toFixed(2)}/mo)`));
90
+ });
91
+ }
92
+ console.log();
93
+ }
94
+ catch (err) {
95
+ (0, logger_1.error)(`Query failed: ${err.message}`);
96
+ process.exit(1);
97
+ }
98
+ }
99
+ function loadRecentScan() {
100
+ const cacheDir = path.join(process.env.HOME || '/tmp', '.cloud-cost-cli', 'scans');
101
+ if (!fs.existsSync(cacheDir)) {
102
+ return null;
103
+ }
104
+ try {
105
+ const files = fs.readdirSync(cacheDir)
106
+ .filter(f => f.endsWith('.json'))
107
+ .map(f => ({
108
+ path: path.join(cacheDir, f),
109
+ stat: fs.statSync(path.join(cacheDir, f)),
110
+ }))
111
+ .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
112
+ if (files.length === 0) {
113
+ return null;
114
+ }
115
+ // Get most recent scan (max 24 hours old)
116
+ const mostRecent = files[0];
117
+ const ageMs = Date.now() - mostRecent.stat.mtimeMs;
118
+ if (ageMs > 24 * 60 * 60 * 1000) {
119
+ return null; // Too old
120
+ }
121
+ const data = fs.readFileSync(mostRecent.path, 'utf-8');
122
+ return JSON.parse(data);
123
+ }
124
+ catch (err) {
125
+ return null;
126
+ }
127
+ }
128
+ function saveScanCache(provider, region, report) {
129
+ const cacheDir = path.join(process.env.HOME || '/tmp', '.cloud-cost-cli', 'scans');
130
+ if (!fs.existsSync(cacheDir)) {
131
+ fs.mkdirSync(cacheDir, { recursive: true });
132
+ }
133
+ const cache = {
134
+ timestamp: Date.now(),
135
+ provider,
136
+ region,
137
+ report,
138
+ };
139
+ const filename = `scan-${provider}-${Date.now()}.json`;
140
+ const filepath = path.join(cacheDir, filename);
141
+ try {
142
+ fs.writeFileSync(filepath, JSON.stringify(cache, null, 2), 'utf-8');
143
+ // Clean up old scans (keep only last 10)
144
+ const files = fs.readdirSync(cacheDir)
145
+ .filter(f => f.endsWith('.json'))
146
+ .map(f => ({
147
+ path: path.join(cacheDir, f),
148
+ stat: fs.statSync(path.join(cacheDir, f)),
149
+ }))
150
+ .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
151
+ // Delete all but the 10 most recent
152
+ files.slice(10).forEach(f => {
153
+ try {
154
+ fs.unlinkSync(f.path);
155
+ }
156
+ catch (err) {
157
+ // Ignore deletion errors
158
+ }
159
+ });
160
+ }
161
+ catch (err) {
162
+ // Failed to save cache, not critical
163
+ }
164
+ }
@@ -0,0 +1 @@
1
+ export declare function configCommand(action: string, key?: string, value?: string): Promise<void>;
@@ -0,0 +1,120 @@
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.configCommand = configCommand;
7
+ const config_1 = require("../utils/config");
8
+ const logger_1 = require("../utils/logger");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ async function configCommand(action, key, value) {
11
+ switch (action) {
12
+ case 'init':
13
+ initConfig();
14
+ break;
15
+ case 'show':
16
+ showConfig();
17
+ break;
18
+ case 'get':
19
+ if (!key) {
20
+ (0, logger_1.error)('Key required for get action');
21
+ process.exit(1);
22
+ }
23
+ getConfigValue(key);
24
+ break;
25
+ case 'set':
26
+ if (!key || !value) {
27
+ (0, logger_1.error)('Key and value required for set action');
28
+ process.exit(1);
29
+ }
30
+ setConfigValue(key, value);
31
+ break;
32
+ case 'path':
33
+ showConfigPath();
34
+ break;
35
+ default:
36
+ (0, logger_1.error)(`Unknown config action: ${action}`);
37
+ (0, logger_1.info)('Available actions: init, show, get, set, path');
38
+ process.exit(1);
39
+ }
40
+ }
41
+ function initConfig() {
42
+ const configPath = config_1.ConfigLoader.getConfigPath();
43
+ const example = config_1.ConfigLoader.generateExample();
44
+ (0, logger_1.info)('Creating example configuration...');
45
+ console.log(chalk_1.default.dim('─'.repeat(80)));
46
+ console.log(example);
47
+ console.log(chalk_1.default.dim('─'.repeat(80)));
48
+ config_1.ConfigLoader.save(JSON.parse(example));
49
+ (0, logger_1.success)(`Configuration saved to: ${configPath}`);
50
+ (0, logger_1.info)('Edit this file to customize your settings');
51
+ }
52
+ function showConfig() {
53
+ const config = config_1.ConfigLoader.load();
54
+ const configPath = config_1.ConfigLoader.getConfigPath();
55
+ console.log(chalk_1.default.bold(`\nConfiguration (from ${configPath}):\n`));
56
+ console.log(JSON.stringify(config, null, 2));
57
+ console.log();
58
+ const errors = config_1.ConfigLoader.validate(config);
59
+ if (errors.length > 0) {
60
+ console.log(chalk_1.default.yellow('⚠️ Validation warnings:'));
61
+ errors.forEach(err => console.log(chalk_1.default.yellow(` - ${err}`)));
62
+ console.log();
63
+ }
64
+ }
65
+ function getConfigValue(key) {
66
+ const config = config_1.ConfigLoader.load();
67
+ const value = getNestedValue(config, key);
68
+ if (value === undefined) {
69
+ (0, logger_1.error)(`Key not found: ${key}`);
70
+ process.exit(1);
71
+ }
72
+ console.log(JSON.stringify(value, null, 2));
73
+ }
74
+ function setConfigValue(key, value) {
75
+ const config = config_1.ConfigLoader.load();
76
+ // Parse value (try JSON first, fall back to string)
77
+ let parsedValue = value;
78
+ try {
79
+ parsedValue = JSON.parse(value);
80
+ }
81
+ catch (e) {
82
+ // Not JSON, use as string
83
+ }
84
+ setNestedValue(config, key, parsedValue);
85
+ const errors = config_1.ConfigLoader.validate(config);
86
+ if (errors.length > 0) {
87
+ (0, logger_1.error)('Configuration validation failed:');
88
+ errors.forEach(err => console.log(chalk_1.default.red(` - ${err}`)));
89
+ process.exit(1);
90
+ }
91
+ config_1.ConfigLoader.save(config);
92
+ (0, logger_1.success)(`Set ${key} = ${JSON.stringify(parsedValue)}`);
93
+ }
94
+ function showConfigPath() {
95
+ const configPath = config_1.ConfigLoader.getConfigPath();
96
+ console.log(configPath);
97
+ }
98
+ function getNestedValue(obj, path) {
99
+ const keys = path.split('.');
100
+ let current = obj;
101
+ for (const key of keys) {
102
+ if (current === undefined || current === null) {
103
+ return undefined;
104
+ }
105
+ current = current[key];
106
+ }
107
+ return current;
108
+ }
109
+ function setNestedValue(obj, path, value) {
110
+ const keys = path.split('.');
111
+ let current = obj;
112
+ for (let i = 0; i < keys.length - 1; i++) {
113
+ const key = keys[i];
114
+ if (current[key] === undefined || typeof current[key] !== 'object') {
115
+ current[key] = {};
116
+ }
117
+ current = current[key];
118
+ }
119
+ current[keys[keys.length - 1]] = value;
120
+ }
@@ -0,0 +1,6 @@
1
+ interface CostsCommandOptions {
2
+ days?: string;
3
+ clear?: boolean;
4
+ }
5
+ export declare function costsCommand(options: CostsCommandOptions): Promise<void>;
6
+ export {};
@@ -0,0 +1,54 @@
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.costsCommand = costsCommand;
7
+ const cost_tracker_1 = require("../utils/cost-tracker");
8
+ const logger_1 = require("../utils/logger");
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ async function costsCommand(options) {
11
+ const tracker = new cost_tracker_1.CostTracker();
12
+ if (options.clear) {
13
+ tracker.clear();
14
+ (0, logger_1.info)('Cost tracking data cleared');
15
+ return;
16
+ }
17
+ const days = parseInt(options.days || '30');
18
+ const summary = tracker.getSummary(days);
19
+ console.log(chalk_1.default.bold(`\n💰 AI Cost Summary (last ${days} days):\n`));
20
+ if (summary.totalOperations === 0) {
21
+ console.log(chalk_1.default.dim('No AI operations tracked yet.'));
22
+ console.log(chalk_1.default.dim('Use --explain or ask commands to generate AI insights.\n'));
23
+ return;
24
+ }
25
+ console.log(chalk_1.default.bold('Total:'));
26
+ console.log(` Operations: ${summary.totalOperations}`);
27
+ console.log(` Cost: $${summary.totalCost.toFixed(4)}`);
28
+ console.log();
29
+ console.log(chalk_1.default.bold('By Provider:'));
30
+ Object.entries(summary.byProvider).forEach(([provider, stats]) => {
31
+ const icon = provider === 'openai' ? '☁️ ' : '🏠';
32
+ console.log(` ${icon} ${provider}:`);
33
+ console.log(` Operations: ${stats.operations}`);
34
+ console.log(` Cost: $${stats.cost.toFixed(4)}`);
35
+ });
36
+ console.log();
37
+ console.log(chalk_1.default.bold('By Model:'));
38
+ Object.entries(summary.byModel).forEach(([model, stats]) => {
39
+ console.log(` ${model}:`);
40
+ console.log(` Operations: ${stats.operations}`);
41
+ console.log(` Cost: $${stats.cost.toFixed(4)}`);
42
+ });
43
+ console.log();
44
+ // Show average cost per operation
45
+ const avgCost = summary.totalCost / summary.totalOperations;
46
+ console.log(chalk_1.default.dim(`Average cost per operation: $${avgCost.toFixed(4)}`));
47
+ // Estimate monthly cost at current rate
48
+ const daysElapsed = days;
49
+ const monthlyEstimate = (summary.totalCost / daysElapsed) * 30;
50
+ if (daysElapsed >= 7) {
51
+ console.log(chalk_1.default.dim(`Estimated monthly cost: $${monthlyEstimate.toFixed(2)}`));
52
+ }
53
+ console.log();
54
+ }
@@ -2,11 +2,17 @@ interface ScanCommandOptions {
2
2
  provider: string;
3
3
  region?: string;
4
4
  profile?: string;
5
+ subscriptionId?: string;
6
+ location?: string;
5
7
  top?: string;
6
8
  output?: string;
7
9
  days?: string;
8
10
  minSavings?: string;
9
11
  verbose?: boolean;
12
+ accurate?: boolean;
13
+ explain?: boolean;
14
+ aiProvider?: string;
15
+ aiModel?: string;
10
16
  }
11
17
  export declare function scanCommand(options: ScanCommandOptions): Promise<void>;
12
18
  export {};