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.
- package/README.md +116 -122
- package/dist/bin/cloud-cost-cli.js +28 -3
- package/dist/src/analyzers/cost-estimator.d.ts +14 -0
- package/dist/src/analyzers/cost-estimator.js +74 -16
- package/dist/src/analyzers/pricing-service.d.ts +38 -0
- package/dist/src/analyzers/pricing-service.js +263 -0
- 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 +6 -0
- package/dist/src/commands/scan.js +255 -85
- package/dist/src/commands/script.d.ts +8 -0
- package/dist/src/commands/script.js +27 -0
- package/dist/src/providers/azure/client.d.ts +20 -0
- package/dist/src/providers/azure/client.js +41 -0
- package/dist/src/providers/azure/disks.d.ts +4 -0
- package/dist/src/providers/azure/disks.js +87 -0
- package/dist/src/providers/azure/index.d.ts +6 -0
- package/dist/src/providers/azure/index.js +15 -0
- package/dist/src/providers/azure/public-ips.d.ts +3 -0
- package/dist/src/providers/azure/public-ips.js +47 -0
- package/dist/src/providers/azure/sql.d.ts +4 -0
- package/dist/src/providers/azure/sql.js +134 -0
- package/dist/src/providers/azure/storage.d.ts +8 -0
- package/dist/src/providers/azure/storage.js +100 -0
- package/dist/src/providers/azure/vms.d.ts +4 -0
- package/dist/src/providers/azure/vms.js +164 -0
- package/dist/src/reporters/table.d.ts +2 -1
- package/dist/src/reporters/table.js +69 -3
- 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/dist/src/utils/formatter.d.ts +2 -0
- package/dist/src/utils/formatter.js +29 -1
- package/docs/RELEASE.md +14 -25
- package/package.json +15 -3
|
@@ -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;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export declare function formatCurrency(amount: number): string;
|
|
2
|
+
export declare function formatBytes(bytes: number, decimals?: number): string;
|
|
3
|
+
export declare function formatPercent(value: number, decimals?: number): string;
|
|
2
4
|
export declare function formatDate(date: Date): string;
|
|
3
5
|
export declare function daysSince(date: Date): number;
|
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatCurrency = formatCurrency;
|
|
4
|
+
exports.formatBytes = formatBytes;
|
|
5
|
+
exports.formatPercent = formatPercent;
|
|
4
6
|
exports.formatDate = formatDate;
|
|
5
7
|
exports.daysSince = daysSince;
|
|
6
8
|
function formatCurrency(amount) {
|
|
7
|
-
|
|
9
|
+
const formatted = new Intl.NumberFormat('en-US', {
|
|
10
|
+
style: 'currency',
|
|
11
|
+
currency: 'USD',
|
|
12
|
+
minimumFractionDigits: 2,
|
|
13
|
+
maximumFractionDigits: 2,
|
|
14
|
+
}).format(amount);
|
|
15
|
+
return formatted;
|
|
16
|
+
}
|
|
17
|
+
function formatBytes(bytes, decimals = 2) {
|
|
18
|
+
if (bytes === 0)
|
|
19
|
+
return '0 B';
|
|
20
|
+
const k = 1024;
|
|
21
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
22
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
23
|
+
// Don't use decimals for bytes
|
|
24
|
+
if (i === 0) {
|
|
25
|
+
return `${bytes} B`;
|
|
26
|
+
}
|
|
27
|
+
const value = bytes / Math.pow(k, i);
|
|
28
|
+
const formatted = decimals === 0
|
|
29
|
+
? Math.round(value).toString()
|
|
30
|
+
: value.toFixed(decimals);
|
|
31
|
+
return `${formatted} ${sizes[i]}`;
|
|
32
|
+
}
|
|
33
|
+
function formatPercent(value, decimals = 1) {
|
|
34
|
+
const percent = value * 100;
|
|
35
|
+
return `${percent.toFixed(decimals)}%`;
|
|
8
36
|
}
|
|
9
37
|
function formatDate(date) {
|
|
10
38
|
return date.toISOString().split('T')[0];
|
package/docs/RELEASE.md
CHANGED
|
@@ -4,31 +4,20 @@ This document describes how to publish a new version of cloud-cost-cli.
|
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- The token itself is still secure (stored in GitHub Secrets)
|
|
22
|
-
4. Click **"Generate Token"**
|
|
23
|
-
5. Copy the token (starts with `npm_...`)
|
|
24
|
-
|
|
25
|
-
### 2. Add Token to GitHub Secrets
|
|
26
|
-
|
|
27
|
-
1. Go to https://github.com/vuhp/cloud-cost-cli/settings/secrets/actions
|
|
28
|
-
2. Click **"New repository secret"** (or edit existing `NPM_TOKEN`)
|
|
29
|
-
3. Name: `NPM_TOKEN`
|
|
30
|
-
4. Value: (paste the `npm_...` token with 2FA disabled)
|
|
31
|
-
5. Click **"Add secret"** or **"Update secret"**
|
|
7
|
+
### npm Trusted Publishing (OIDC)
|
|
8
|
+
|
|
9
|
+
This project uses **npm trusted publishing** via GitHub Actions. No npm token is required!
|
|
10
|
+
|
|
11
|
+
**Configuration:**
|
|
12
|
+
- Already configured on npm for this package
|
|
13
|
+
- GitHub Actions is set as a trusted publisher
|
|
14
|
+
- Publishing happens automatically via OIDC (OpenID Connect)
|
|
15
|
+
|
|
16
|
+
**Benefits:**
|
|
17
|
+
- ✅ No tokens to rotate or secure
|
|
18
|
+
- ✅ No 2FA required for automation
|
|
19
|
+
- ✅ More secure (GitHub's identity proves authenticity)
|
|
20
|
+
- ✅ Automatic provenance statements
|
|
32
21
|
|
|
33
22
|
## Steps to Release
|
|
34
23
|
|