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.
- package/dist/bin/cloud-cost-cli.js +23 -1
- package/dist/src/commands/ask.d.ts +11 -0
- package/dist/src/commands/ask.js +164 -0
- package/dist/src/commands/config.d.ts +1 -0
- package/dist/src/commands/config.js +120 -0
- package/dist/src/commands/costs.d.ts +6 -0
- package/dist/src/commands/costs.js +54 -0
- package/dist/src/commands/scan.d.ts +3 -0
- package/dist/src/commands/scan.js +85 -2
- package/dist/src/commands/script.d.ts +8 -0
- package/dist/src/commands/script.js +27 -0
- package/dist/src/reporters/table.d.ts +2 -1
- package/dist/src/reporters/table.js +66 -2
- package/dist/src/services/ai.d.ts +44 -0
- package/dist/src/services/ai.js +345 -0
- package/dist/src/services/script-generator.d.ts +21 -0
- package/dist/src/services/script-generator.js +245 -0
- package/dist/src/utils/cache.d.ts +25 -0
- package/dist/src/utils/cache.js +197 -0
- package/dist/src/utils/config.d.ts +37 -0
- package/dist/src/utils/config.js +175 -0
- package/dist/src/utils/cost-tracker.d.ts +33 -0
- package/dist/src/utils/cost-tracker.js +135 -0
- package/package.json +4 -2
|
@@ -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.
|
|
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",
|