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,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
- return `$${amount.toFixed(2)}`;
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
- ### 1. NPM Granular Access Token
8
-
9
- npm now uses "Granular Access Tokens" instead of automation tokens.
10
-
11
- **Create the token:**
12
- 1. Go to https://www.npmjs.com/settings/YOUR_USERNAME/tokens
13
- 2. Click **"Generate New Token"** **"Granular Access Token"**
14
- 3. Configure the token:
15
- - **Token name**: `cloud-cost-cli-publish` (or any name)
16
- - **Expiration**: 1 year (or No expiration)
17
- - **Packages and scopes**: Select packages Choose `cloud-cost-cli`
18
- - **Permissions**: Read and write
19
- - **⚠️ IMPORTANT**: Under "Additional options" **Disable "Require two-factor authentication for this token"**
20
- - This allows GitHub Actions to publish without OTP
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