cloud-cost-cli 0.2.0 → 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.
@@ -0,0 +1,197 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ExplanationCache = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ class ExplanationCache {
41
+ cacheDir;
42
+ cacheDuration; // in milliseconds
43
+ constructor(cacheDuration = 7 * 24 * 60 * 60 * 1000) {
44
+ this.cacheDir = path.join(process.env.HOME || '/tmp', '.cloud-cost-cli', 'cache');
45
+ this.cacheDuration = cacheDuration;
46
+ this.ensureCacheDir();
47
+ }
48
+ ensureCacheDir() {
49
+ if (!fs.existsSync(this.cacheDir)) {
50
+ fs.mkdirSync(this.cacheDir, { recursive: true });
51
+ }
52
+ }
53
+ hashOpportunity(data) {
54
+ const str = JSON.stringify({
55
+ provider: data.provider,
56
+ resourceType: data.resourceType,
57
+ category: data.category,
58
+ recommendation: data.recommendation,
59
+ // Include key metadata that affects the explanation
60
+ metadata: {
61
+ avgCpu: data.metadata?.avgCpu,
62
+ size: data.metadata?.size,
63
+ tier: data.metadata?.tier,
64
+ },
65
+ });
66
+ return crypto.createHash('sha256').update(str).digest('hex').substring(0, 16);
67
+ }
68
+ getCachePath(hash) {
69
+ return path.join(this.cacheDir, `${hash}.json`);
70
+ }
71
+ get(opportunityData, provider, model) {
72
+ const hash = this.hashOpportunity(opportunityData);
73
+ const cachePath = this.getCachePath(hash);
74
+ if (!fs.existsSync(cachePath)) {
75
+ return null;
76
+ }
77
+ try {
78
+ const data = fs.readFileSync(cachePath, 'utf-8');
79
+ const cached = JSON.parse(data);
80
+ // Check if cache is expired
81
+ const age = Date.now() - cached.timestamp;
82
+ if (age > this.cacheDuration) {
83
+ // Cache expired, delete it
84
+ fs.unlinkSync(cachePath);
85
+ return null;
86
+ }
87
+ // Check if provider/model match
88
+ if (cached.provider !== provider || cached.model !== model) {
89
+ return null;
90
+ }
91
+ return cached.explanation;
92
+ }
93
+ catch (error) {
94
+ // Invalid cache file, delete it
95
+ try {
96
+ fs.unlinkSync(cachePath);
97
+ }
98
+ catch (e) {
99
+ // Ignore deletion errors
100
+ }
101
+ return null;
102
+ }
103
+ }
104
+ set(opportunityData, provider, model, explanation) {
105
+ const hash = this.hashOpportunity(opportunityData);
106
+ const cachePath = this.getCachePath(hash);
107
+ const cached = {
108
+ opportunityHash: hash,
109
+ explanation,
110
+ timestamp: Date.now(),
111
+ provider,
112
+ model,
113
+ };
114
+ try {
115
+ fs.writeFileSync(cachePath, JSON.stringify(cached, null, 2), 'utf-8');
116
+ }
117
+ catch (error) {
118
+ // Failed to write cache, not critical - continue without caching
119
+ }
120
+ }
121
+ clear() {
122
+ let count = 0;
123
+ try {
124
+ const files = fs.readdirSync(this.cacheDir);
125
+ for (const file of files) {
126
+ if (file.endsWith('.json')) {
127
+ fs.unlinkSync(path.join(this.cacheDir, file));
128
+ count++;
129
+ }
130
+ }
131
+ }
132
+ catch (error) {
133
+ // Directory doesn't exist or can't be read
134
+ }
135
+ return count;
136
+ }
137
+ clearExpired() {
138
+ let count = 0;
139
+ try {
140
+ const files = fs.readdirSync(this.cacheDir);
141
+ for (const file of files) {
142
+ if (!file.endsWith('.json'))
143
+ continue;
144
+ const filePath = path.join(this.cacheDir, file);
145
+ try {
146
+ const data = fs.readFileSync(filePath, 'utf-8');
147
+ const cached = JSON.parse(data);
148
+ const age = Date.now() - cached.timestamp;
149
+ if (age > this.cacheDuration) {
150
+ fs.unlinkSync(filePath);
151
+ count++;
152
+ }
153
+ }
154
+ catch (error) {
155
+ // Invalid file, delete it
156
+ fs.unlinkSync(filePath);
157
+ count++;
158
+ }
159
+ }
160
+ }
161
+ catch (error) {
162
+ // Directory doesn't exist or can't be read
163
+ }
164
+ return count;
165
+ }
166
+ getStats() {
167
+ let total = 0;
168
+ let size = 0;
169
+ let oldest = null;
170
+ try {
171
+ const files = fs.readdirSync(this.cacheDir);
172
+ for (const file of files) {
173
+ if (!file.endsWith('.json'))
174
+ continue;
175
+ const filePath = path.join(this.cacheDir, file);
176
+ try {
177
+ const stats = fs.statSync(filePath);
178
+ size += stats.size;
179
+ total++;
180
+ const data = fs.readFileSync(filePath, 'utf-8');
181
+ const cached = JSON.parse(data);
182
+ if (oldest === null || cached.timestamp < oldest) {
183
+ oldest = cached.timestamp;
184
+ }
185
+ }
186
+ catch (error) {
187
+ // Skip invalid files
188
+ }
189
+ }
190
+ }
191
+ catch (error) {
192
+ // Directory doesn't exist
193
+ }
194
+ return { total, size, oldest };
195
+ }
196
+ }
197
+ exports.ExplanationCache = ExplanationCache;
@@ -0,0 +1,37 @@
1
+ export interface Config {
2
+ ai?: {
3
+ provider?: 'openai' | 'ollama';
4
+ apiKey?: string;
5
+ model?: string;
6
+ maxExplanations?: number;
7
+ cache?: {
8
+ enabled?: boolean;
9
+ ttlDays?: number;
10
+ };
11
+ };
12
+ scan?: {
13
+ defaultProvider?: 'aws' | 'azure';
14
+ defaultRegion?: string;
15
+ defaultTop?: number;
16
+ minSavings?: number;
17
+ };
18
+ aws?: {
19
+ profile?: string;
20
+ region?: string;
21
+ };
22
+ azure?: {
23
+ subscriptionId?: string;
24
+ location?: string;
25
+ };
26
+ }
27
+ export declare class ConfigLoader {
28
+ private static CONFIG_FILENAME;
29
+ private static DEFAULT_CONFIG;
30
+ static load(): Config;
31
+ static save(config: Config): void;
32
+ static getConfigPath(): string;
33
+ private static findConfigFile;
34
+ private static mergeConfig;
35
+ static validate(config: Config): string[];
36
+ static generateExample(): string;
37
+ }
@@ -0,0 +1,175 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ConfigLoader = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ class ConfigLoader {
41
+ static CONFIG_FILENAME = '.cloud-cost-cli.json';
42
+ static DEFAULT_CONFIG = {
43
+ ai: {
44
+ provider: 'openai',
45
+ maxExplanations: 3,
46
+ cache: {
47
+ enabled: true,
48
+ ttlDays: 7,
49
+ },
50
+ },
51
+ scan: {
52
+ defaultProvider: 'aws',
53
+ defaultTop: 5,
54
+ },
55
+ };
56
+ static load() {
57
+ const configPath = this.findConfigFile();
58
+ if (!configPath) {
59
+ return this.DEFAULT_CONFIG;
60
+ }
61
+ try {
62
+ const content = fs.readFileSync(configPath, 'utf-8');
63
+ const userConfig = JSON.parse(content);
64
+ // Merge with defaults
65
+ return this.mergeConfig(this.DEFAULT_CONFIG, userConfig);
66
+ }
67
+ catch (error) {
68
+ console.warn(`Warning: Failed to load config from ${configPath}: ${error.message}`);
69
+ return this.DEFAULT_CONFIG;
70
+ }
71
+ }
72
+ static save(config) {
73
+ const configPath = path.join(os.homedir(), this.CONFIG_FILENAME);
74
+ try {
75
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
76
+ }
77
+ catch (error) {
78
+ throw new Error(`Failed to save config: ${error.message}`);
79
+ }
80
+ }
81
+ static getConfigPath() {
82
+ return this.findConfigFile() || path.join(os.homedir(), this.CONFIG_FILENAME);
83
+ }
84
+ static findConfigFile() {
85
+ // Search order:
86
+ // 1. Current directory
87
+ // 2. Home directory
88
+ // 3. XDG_CONFIG_HOME/cloud-cost-cli/
89
+ const searchPaths = [
90
+ path.join(process.cwd(), this.CONFIG_FILENAME),
91
+ path.join(os.homedir(), this.CONFIG_FILENAME),
92
+ path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'cloud-cost-cli', 'config.json'),
93
+ ];
94
+ for (const configPath of searchPaths) {
95
+ if (fs.existsSync(configPath)) {
96
+ return configPath;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+ static mergeConfig(defaults, user) {
102
+ return {
103
+ ai: {
104
+ ...defaults.ai,
105
+ ...user.ai,
106
+ cache: {
107
+ ...defaults.ai?.cache,
108
+ ...user.ai?.cache,
109
+ },
110
+ },
111
+ scan: {
112
+ ...defaults.scan,
113
+ ...user.scan,
114
+ },
115
+ aws: {
116
+ ...defaults.aws,
117
+ ...user.aws,
118
+ },
119
+ azure: {
120
+ ...defaults.azure,
121
+ ...user.azure,
122
+ },
123
+ };
124
+ }
125
+ static validate(config) {
126
+ const errors = [];
127
+ if (config.ai?.provider && !['openai', 'ollama'].includes(config.ai.provider)) {
128
+ errors.push(`Invalid AI provider: ${config.ai.provider}. Must be 'openai' or 'ollama'.`);
129
+ }
130
+ if (config.ai?.maxExplanations !== undefined) {
131
+ if (config.ai.maxExplanations < 1 || config.ai.maxExplanations > 10) {
132
+ errors.push('ai.maxExplanations must be between 1 and 10');
133
+ }
134
+ }
135
+ if (config.ai?.cache?.ttlDays !== undefined) {
136
+ if (config.ai.cache.ttlDays < 1 || config.ai.cache.ttlDays > 365) {
137
+ errors.push('ai.cache.ttlDays must be between 1 and 365');
138
+ }
139
+ }
140
+ if (config.scan?.defaultProvider && !['aws', 'azure'].includes(config.scan.defaultProvider)) {
141
+ errors.push(`Invalid default provider: ${config.scan.defaultProvider}`);
142
+ }
143
+ return errors;
144
+ }
145
+ static generateExample() {
146
+ const example = {
147
+ ai: {
148
+ provider: 'openai',
149
+ apiKey: 'sk-your-openai-key-here',
150
+ model: 'gpt-4o-mini',
151
+ maxExplanations: 3,
152
+ cache: {
153
+ enabled: true,
154
+ ttlDays: 7,
155
+ },
156
+ },
157
+ scan: {
158
+ defaultProvider: 'aws',
159
+ defaultRegion: 'us-east-1',
160
+ defaultTop: 5,
161
+ minSavings: 10,
162
+ },
163
+ aws: {
164
+ profile: 'default',
165
+ region: 'us-east-1',
166
+ },
167
+ azure: {
168
+ subscriptionId: 'your-subscription-id',
169
+ location: 'eastus',
170
+ },
171
+ };
172
+ return JSON.stringify(example, null, 2);
173
+ }
174
+ }
175
+ exports.ConfigLoader = ConfigLoader;
@@ -0,0 +1,33 @@
1
+ export interface CostEntry {
2
+ timestamp: number;
3
+ provider: 'openai' | 'ollama';
4
+ model: string;
5
+ operation: 'explanation' | 'query';
6
+ inputTokens: number;
7
+ outputTokens: number;
8
+ estimatedCost: number;
9
+ }
10
+ export interface CostSummary {
11
+ totalCost: number;
12
+ totalOperations: number;
13
+ byProvider: {
14
+ [key: string]: {
15
+ cost: number;
16
+ operations: number;
17
+ };
18
+ };
19
+ byModel: {
20
+ [key: string]: {
21
+ cost: number;
22
+ operations: number;
23
+ };
24
+ };
25
+ }
26
+ export declare class CostTracker {
27
+ private logPath;
28
+ constructor();
29
+ track(entry: Omit<CostEntry, 'timestamp' | 'estimatedCost'>): void;
30
+ getSummary(daysBack?: number): CostSummary;
31
+ clear(): void;
32
+ private calculateCost;
33
+ }
@@ -0,0 +1,135 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CostTracker = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ class CostTracker {
41
+ logPath;
42
+ constructor() {
43
+ const logDir = path.join(os.homedir(), '.cloud-cost-cli', 'costs');
44
+ if (!fs.existsSync(logDir)) {
45
+ fs.mkdirSync(logDir, { recursive: true });
46
+ }
47
+ this.logPath = path.join(logDir, 'usage.jsonl');
48
+ }
49
+ track(entry) {
50
+ const cost = this.calculateCost(entry.provider, entry.model, entry.inputTokens, entry.outputTokens);
51
+ const fullEntry = {
52
+ ...entry,
53
+ timestamp: Date.now(),
54
+ estimatedCost: cost,
55
+ };
56
+ try {
57
+ fs.appendFileSync(this.logPath, JSON.stringify(fullEntry) + '\n', 'utf-8');
58
+ }
59
+ catch (error) {
60
+ // Failed to log, not critical
61
+ }
62
+ }
63
+ getSummary(daysBack = 30) {
64
+ const cutoff = Date.now() - (daysBack * 24 * 60 * 60 * 1000);
65
+ const summary = {
66
+ totalCost: 0,
67
+ totalOperations: 0,
68
+ byProvider: {},
69
+ byModel: {},
70
+ };
71
+ if (!fs.existsSync(this.logPath)) {
72
+ return summary;
73
+ }
74
+ try {
75
+ const content = fs.readFileSync(this.logPath, 'utf-8');
76
+ const lines = content.trim().split('\n').filter(l => l.length > 0);
77
+ for (const line of lines) {
78
+ try {
79
+ const entry = JSON.parse(line);
80
+ if (entry.timestamp < cutoff) {
81
+ continue;
82
+ }
83
+ summary.totalCost += entry.estimatedCost;
84
+ summary.totalOperations++;
85
+ // By provider
86
+ if (!summary.byProvider[entry.provider]) {
87
+ summary.byProvider[entry.provider] = { cost: 0, operations: 0 };
88
+ }
89
+ summary.byProvider[entry.provider].cost += entry.estimatedCost;
90
+ summary.byProvider[entry.provider].operations++;
91
+ // By model
92
+ if (!summary.byModel[entry.model]) {
93
+ summary.byModel[entry.model] = { cost: 0, operations: 0 };
94
+ }
95
+ summary.byModel[entry.model].cost += entry.estimatedCost;
96
+ summary.byModel[entry.model].operations++;
97
+ }
98
+ catch (e) {
99
+ // Skip invalid lines
100
+ }
101
+ }
102
+ }
103
+ catch (error) {
104
+ // Failed to read log
105
+ }
106
+ return summary;
107
+ }
108
+ clear() {
109
+ try {
110
+ if (fs.existsSync(this.logPath)) {
111
+ fs.unlinkSync(this.logPath);
112
+ }
113
+ }
114
+ catch (error) {
115
+ // Failed to clear
116
+ }
117
+ }
118
+ calculateCost(provider, model, inputTokens, outputTokens) {
119
+ if (provider === 'ollama') {
120
+ return 0; // Local, free
121
+ }
122
+ // OpenAI pricing (as of 2024)
123
+ // https://openai.com/pricing
124
+ const pricing = {
125
+ 'gpt-4o': { input: 2.50 / 1_000_000, output: 10.00 / 1_000_000 },
126
+ 'gpt-4o-mini': { input: 0.150 / 1_000_000, output: 0.600 / 1_000_000 },
127
+ 'gpt-4-turbo': { input: 10.00 / 1_000_000, output: 30.00 / 1_000_000 },
128
+ 'gpt-4': { input: 30.00 / 1_000_000, output: 60.00 / 1_000_000 },
129
+ 'gpt-3.5-turbo': { input: 0.50 / 1_000_000, output: 1.50 / 1_000_000 },
130
+ };
131
+ const modelPricing = pricing[model] || pricing['gpt-4o-mini']; // Default fallback
132
+ return (inputTokens * modelPricing.input) + (outputTokens * modelPricing.output);
133
+ }
134
+ }
135
+ exports.CostTracker = CostTracker;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloud-cost-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "Optimize your cloud spend in seconds",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -62,7 +62,9 @@
62
62
  "cli-table3": "^0.6.5",
63
63
  "commander": "^12.1.0",
64
64
  "dayjs": "^1.11.13",
65
- "ini": "^6.0.0"
65
+ "ini": "^6.0.0",
66
+ "ollama": "^0.6.3",
67
+ "openai": "^6.17.0"
66
68
  },
67
69
  "devDependencies": {
68
70
  "@types/ini": "^4.1.1",