@jadenrazo/cloudcost-mcp 0.1.0

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/index.js ADDED
@@ -0,0 +1,4488 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getAwsInstances,
4
+ getAzureVmSizes,
5
+ getGcpComputePricing,
6
+ getGcpDiskPricing,
7
+ getGcpMachineTypes,
8
+ getGcpSqlPricing,
9
+ getGcpStoragePricing,
10
+ getInstanceMap,
11
+ getRegionMappings,
12
+ getResourceEquivalents,
13
+ getStorageMap
14
+ } from "./chunk-KZJSZMWM.js";
15
+
16
+ // src/server.ts
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+
20
+ // src/config.ts
21
+ import { readFileSync, existsSync } from "fs";
22
+ import { join } from "path";
23
+ import { homedir } from "os";
24
+
25
+ // src/types/config.ts
26
+ var DEFAULT_CONFIG = {
27
+ cache: {
28
+ ttl_seconds: 86400,
29
+ db_path: ""
30
+ },
31
+ pricing: {
32
+ monthly_hours: 730,
33
+ default_currency: "USD",
34
+ aws_pricing_region: "us-east-1"
35
+ },
36
+ logging: {
37
+ level: "info"
38
+ }
39
+ };
40
+
41
+ // src/config.ts
42
+ function getConfigDir() {
43
+ return join(homedir(), ".cloudcost");
44
+ }
45
+ function loadFileConfig() {
46
+ const configPath = join(getConfigDir(), "config.json");
47
+ if (!existsSync(configPath)) return {};
48
+ try {
49
+ return JSON.parse(readFileSync(configPath, "utf-8"));
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+ function loadEnvConfig() {
55
+ const config = {};
56
+ const env = process.env;
57
+ if (env.CLOUDCOST_CACHE_TTL || env.CLOUDCOST_CACHE_PATH) {
58
+ config.cache = {
59
+ ttl_seconds: env.CLOUDCOST_CACHE_TTL ? parseInt(env.CLOUDCOST_CACHE_TTL, 10) : DEFAULT_CONFIG.cache.ttl_seconds,
60
+ db_path: env.CLOUDCOST_CACHE_PATH ?? ""
61
+ };
62
+ }
63
+ if (env.CLOUDCOST_LOG_LEVEL) {
64
+ config.logging = {
65
+ level: env.CLOUDCOST_LOG_LEVEL
66
+ };
67
+ }
68
+ if (env.CLOUDCOST_MONTHLY_HOURS) {
69
+ config.pricing = {
70
+ ...DEFAULT_CONFIG.pricing,
71
+ monthly_hours: parseInt(env.CLOUDCOST_MONTHLY_HOURS, 10)
72
+ };
73
+ }
74
+ return config;
75
+ }
76
+ function loadConfig() {
77
+ const fileConfig = loadFileConfig();
78
+ const envConfig = loadEnvConfig();
79
+ const defaultDbPath = join(getConfigDir(), "cache.db");
80
+ return {
81
+ cache: {
82
+ ...DEFAULT_CONFIG.cache,
83
+ db_path: defaultDbPath,
84
+ ...fileConfig.cache,
85
+ ...envConfig.cache
86
+ },
87
+ pricing: {
88
+ ...DEFAULT_CONFIG.pricing,
89
+ ...fileConfig.pricing,
90
+ ...envConfig.pricing
91
+ },
92
+ logging: {
93
+ ...DEFAULT_CONFIG.logging,
94
+ ...fileConfig.logging,
95
+ ...envConfig.logging
96
+ }
97
+ };
98
+ }
99
+
100
+ // src/logger.ts
101
+ var LEVEL_ORDER = {
102
+ debug: 0,
103
+ info: 1,
104
+ warn: 2,
105
+ error: 3
106
+ };
107
+ var currentLevel = "info";
108
+ function setLogLevel(level) {
109
+ currentLevel = level;
110
+ }
111
+ function shouldLog(level) {
112
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
113
+ }
114
+ function log(level, message, data) {
115
+ if (!shouldLog(level)) return;
116
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
117
+ const entry = data ? `[${timestamp}] ${level.toUpperCase()}: ${message} ${JSON.stringify(data)}` : `[${timestamp}] ${level.toUpperCase()}: ${message}`;
118
+ process.stderr.write(entry + "\n");
119
+ }
120
+ var logger = {
121
+ debug: (msg, data) => log("debug", msg, data),
122
+ info: (msg, data) => log("info", msg, data),
123
+ warn: (msg, data) => log("warn", msg, data),
124
+ error: (msg, data) => log("error", msg, data)
125
+ };
126
+
127
+ // src/pricing/cache.ts
128
+ import Database from "better-sqlite3";
129
+ import { mkdirSync } from "fs";
130
+ import { dirname } from "path";
131
+ var SCHEMA = `
132
+ CREATE TABLE IF NOT EXISTS pricing_cache (
133
+ key TEXT PRIMARY KEY,
134
+ data TEXT NOT NULL,
135
+ provider TEXT NOT NULL,
136
+ service TEXT NOT NULL,
137
+ region TEXT NOT NULL,
138
+ created_at TEXT NOT NULL,
139
+ expires_at TEXT NOT NULL
140
+ );
141
+ CREATE INDEX IF NOT EXISTS idx_pricing_cache_provider ON pricing_cache (provider);
142
+ CREATE INDEX IF NOT EXISTS idx_pricing_cache_expires_at ON pricing_cache (expires_at);
143
+ `;
144
+ var PricingCache = class {
145
+ db;
146
+ constructor(dbPath) {
147
+ mkdirSync(dirname(dbPath), { recursive: true });
148
+ this.db = new Database(dbPath);
149
+ this.db.pragma("journal_mode = WAL");
150
+ this.db.pragma("foreign_keys = ON");
151
+ this.db.exec(SCHEMA);
152
+ logger.debug("PricingCache initialised", { dbPath });
153
+ }
154
+ /**
155
+ * Retrieve a cached value by key. Returns null when the entry does not exist
156
+ * or has already expired; expired rows are deleted as a side-effect so the
157
+ * table self-trims on read.
158
+ */
159
+ get(key) {
160
+ const row = this.db.prepare(
161
+ "SELECT * FROM pricing_cache WHERE key = ?"
162
+ ).get(key);
163
+ if (!row) return null;
164
+ const now = (/* @__PURE__ */ new Date()).toISOString();
165
+ if (row.expires_at <= now) {
166
+ this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
167
+ logger.debug("Cache miss (expired)", { key });
168
+ return null;
169
+ }
170
+ logger.debug("Cache hit", { key });
171
+ return JSON.parse(row.data);
172
+ }
173
+ /**
174
+ * Store a value in the cache.
175
+ *
176
+ * The composite key convention for pricing lookups is:
177
+ * `{provider}/{service}/{region}/{resource_type}`
178
+ *
179
+ * Callers are responsible for constructing the key before calling this
180
+ * method; the method itself treats the key as an opaque string so it can
181
+ * be reused for other cache domains in the future.
182
+ */
183
+ set(key, data, provider, service, region, ttlSeconds) {
184
+ const now = /* @__PURE__ */ new Date();
185
+ const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
186
+ this.db.prepare(
187
+ `INSERT INTO pricing_cache (key, data, provider, service, region, created_at, expires_at)
188
+ VALUES (?, ?, ?, ?, ?, ?, ?)
189
+ ON CONFLICT (key) DO UPDATE SET
190
+ data = excluded.data,
191
+ provider = excluded.provider,
192
+ service = excluded.service,
193
+ region = excluded.region,
194
+ created_at = excluded.created_at,
195
+ expires_at = excluded.expires_at`
196
+ ).run(
197
+ key,
198
+ JSON.stringify(data),
199
+ provider,
200
+ service,
201
+ region,
202
+ now.toISOString(),
203
+ expiresAt.toISOString()
204
+ );
205
+ logger.debug("Cache set", { key, ttlSeconds });
206
+ }
207
+ /** Remove a single cache entry. */
208
+ invalidate(key) {
209
+ const result = this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
210
+ logger.debug("Cache invalidated", { key, removed: result.changes });
211
+ }
212
+ /** Remove every cache entry belonging to a provider. */
213
+ invalidateByProvider(provider) {
214
+ const result = this.db.prepare("DELETE FROM pricing_cache WHERE provider = ?").run(provider);
215
+ logger.debug("Cache invalidated by provider", {
216
+ provider,
217
+ removed: result.changes
218
+ });
219
+ }
220
+ /**
221
+ * Delete all entries whose expiry timestamp is in the past.
222
+ * Returns the number of rows removed.
223
+ */
224
+ cleanup() {
225
+ const now = (/* @__PURE__ */ new Date()).toISOString();
226
+ const result = this.db.prepare("DELETE FROM pricing_cache WHERE expires_at <= ?").run(now);
227
+ const removed = result.changes;
228
+ logger.debug("Cache cleanup complete", { removed });
229
+ return removed;
230
+ }
231
+ /** Return aggregate statistics about the current cache state. */
232
+ getStats() {
233
+ const now = (/* @__PURE__ */ new Date()).toISOString();
234
+ const totals = this.db.prepare(
235
+ `SELECT
236
+ COUNT(*) AS total_entries,
237
+ COALESCE(SUM(LENGTH(data)), 0) AS size_bytes
238
+ FROM pricing_cache`
239
+ ).get();
240
+ const expired = this.db.prepare(
241
+ "SELECT COUNT(*) AS expired_entries FROM pricing_cache WHERE expires_at <= ?"
242
+ ).get(now);
243
+ return {
244
+ total_entries: totals.total_entries,
245
+ expired_entries: expired.expired_entries,
246
+ size_bytes: totals.size_bytes
247
+ };
248
+ }
249
+ /** Close the underlying database connection. */
250
+ close() {
251
+ this.db.close();
252
+ logger.debug("PricingCache closed");
253
+ }
254
+ };
255
+
256
+ // src/pricing/aws/aws-normalizer.ts
257
+ function extractUsdPrice(priceDimensions) {
258
+ for (const dim of Object.values(priceDimensions)) {
259
+ const usd = dim?.pricePerUnit?.USD;
260
+ if (usd !== void 0) {
261
+ const val = parseFloat(usd);
262
+ if (!isNaN(val)) return val;
263
+ }
264
+ }
265
+ return 0;
266
+ }
267
+ function extractUnit(priceDimensions) {
268
+ for (const dim of Object.values(priceDimensions)) {
269
+ if (dim?.unit) return String(dim.unit);
270
+ }
271
+ return "Hrs";
272
+ }
273
+ function normalizeAwsCompute(rawProduct, rawPrice, region) {
274
+ const attrs = rawProduct?.attributes ?? {};
275
+ const terms = rawPrice?.terms?.OnDemand ?? {};
276
+ let price = 0;
277
+ let unit = "Hrs";
278
+ for (const term of Object.values(terms)) {
279
+ const dims = term?.priceDimensions ?? {};
280
+ price = extractUsdPrice(dims);
281
+ unit = extractUnit(dims);
282
+ break;
283
+ }
284
+ return {
285
+ provider: "aws",
286
+ service: "ec2",
287
+ resource_type: attrs.instanceType ?? "unknown",
288
+ region,
289
+ unit,
290
+ price_per_unit: price,
291
+ currency: "USD",
292
+ description: attrs.instanceType ? `AWS EC2 ${attrs.instanceType} (${attrs.operatingSystem ?? "Linux"})` : void 0,
293
+ attributes: {
294
+ instance_type: attrs.instanceType ?? "",
295
+ vcpu: attrs.vcpu ?? "",
296
+ memory: attrs.memory ?? "",
297
+ operating_system: attrs.operatingSystem ?? "Linux",
298
+ tenancy: attrs.tenancy ?? "Shared"
299
+ },
300
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
301
+ };
302
+ }
303
+ function normalizeAwsDatabase(rawProduct, rawPrice, region) {
304
+ const attrs = rawProduct?.attributes ?? {};
305
+ const terms = rawPrice?.terms?.OnDemand ?? {};
306
+ let price = 0;
307
+ let unit = "Hrs";
308
+ for (const term of Object.values(terms)) {
309
+ const dims = term?.priceDimensions ?? {};
310
+ price = extractUsdPrice(dims);
311
+ unit = extractUnit(dims);
312
+ break;
313
+ }
314
+ return {
315
+ provider: "aws",
316
+ service: "rds",
317
+ resource_type: attrs.instanceType ?? "unknown",
318
+ region,
319
+ unit,
320
+ price_per_unit: price,
321
+ currency: "USD",
322
+ description: attrs.instanceType ? `AWS RDS ${attrs.instanceType} (${attrs.databaseEngine ?? "MySQL"})` : void 0,
323
+ attributes: {
324
+ instance_type: attrs.instanceType ?? "",
325
+ database_engine: attrs.databaseEngine ?? "",
326
+ deployment_option: attrs.deploymentOption ?? "Single-AZ",
327
+ vcpu: attrs.vcpu ?? "",
328
+ memory: attrs.memory ?? ""
329
+ },
330
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
331
+ };
332
+ }
333
+ function normalizeAwsStorage(rawProduct, rawPrice, region) {
334
+ const attrs = rawProduct?.attributes ?? {};
335
+ const terms = rawPrice?.terms?.OnDemand ?? {};
336
+ let price = 0;
337
+ let unit = "GB-Mo";
338
+ for (const term of Object.values(terms)) {
339
+ const dims = term?.priceDimensions ?? {};
340
+ price = extractUsdPrice(dims);
341
+ unit = extractUnit(dims);
342
+ break;
343
+ }
344
+ const volumeType = attrs.volumeApiName ?? attrs.volumeType ?? "gp3";
345
+ return {
346
+ provider: "aws",
347
+ service: "ebs",
348
+ resource_type: volumeType,
349
+ region,
350
+ unit,
351
+ price_per_unit: price,
352
+ currency: "USD",
353
+ description: `AWS EBS ${volumeType} volume`,
354
+ attributes: {
355
+ volume_type: volumeType,
356
+ max_iops: attrs.maxIopsvolume ?? "",
357
+ max_throughput: attrs.maxThroughputvolume ?? ""
358
+ },
359
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
360
+ };
361
+ }
362
+
363
+ // src/pricing/aws/bulk-loader.ts
364
+ function parseCsvLine(line) {
365
+ const fields = [];
366
+ let i = 0;
367
+ const len = line.length;
368
+ while (i <= len) {
369
+ if (i === len) {
370
+ if (fields.length > 0 && line[len - 1] === ",") {
371
+ fields.push("");
372
+ }
373
+ break;
374
+ }
375
+ if (line[i] === '"') {
376
+ i++;
377
+ let field = "";
378
+ while (i < len) {
379
+ if (line[i] === '"') {
380
+ if (i + 1 < len && line[i + 1] === '"') {
381
+ field += '"';
382
+ i += 2;
383
+ } else {
384
+ i++;
385
+ break;
386
+ }
387
+ } else {
388
+ field += line[i];
389
+ i++;
390
+ }
391
+ }
392
+ fields.push(field);
393
+ if (i < len && line[i] === ",") i++;
394
+ } else {
395
+ const start = i;
396
+ while (i < len && line[i] !== ",") i++;
397
+ fields.push(line.slice(start, i));
398
+ if (i < len) i++;
399
+ }
400
+ }
401
+ return fields;
402
+ }
403
+ var EC2_BASE_PRICES = {
404
+ "t2.micro": 0.0116,
405
+ "t2.small": 0.023,
406
+ "t2.medium": 0.0464,
407
+ "t2.large": 0.0928,
408
+ "t3.micro": 0.0104,
409
+ "t3.small": 0.0208,
410
+ "t3.medium": 0.0416,
411
+ "t3.large": 0.0832,
412
+ "t3.xlarge": 0.1664,
413
+ "t3.2xlarge": 0.3328,
414
+ "t3a.micro": 94e-4,
415
+ "t3a.small": 0.0188,
416
+ "t3a.medium": 0.0376,
417
+ "t3a.large": 0.0752,
418
+ "t3a.xlarge": 0.1504,
419
+ "t4g.micro": 84e-4,
420
+ "t4g.small": 0.0168,
421
+ "t4g.medium": 0.0336,
422
+ "t4g.large": 0.0672,
423
+ "t4g.xlarge": 0.1344,
424
+ "m5.large": 0.096,
425
+ "m5.xlarge": 0.192,
426
+ "m5.2xlarge": 0.384,
427
+ "m5.4xlarge": 0.768,
428
+ "m5a.large": 0.086,
429
+ "m5a.xlarge": 0.172,
430
+ "m5a.2xlarge": 0.344,
431
+ "m5a.4xlarge": 0.688,
432
+ "m6i.large": 0.096,
433
+ "m6i.xlarge": 0.192,
434
+ "m6i.2xlarge": 0.384,
435
+ "m6i.4xlarge": 0.768,
436
+ "m6i.8xlarge": 1.536,
437
+ "m6g.large": 0.077,
438
+ "m6g.xlarge": 0.154,
439
+ "m6g.2xlarge": 0.308,
440
+ "m6g.4xlarge": 0.616,
441
+ "m7i.large": 0.1008,
442
+ "m7i.xlarge": 0.2016,
443
+ "m7i.2xlarge": 0.4032,
444
+ "m7g.large": 0.0816,
445
+ "m7g.xlarge": 0.1632,
446
+ "m7g.2xlarge": 0.3264,
447
+ "m7g.4xlarge": 0.6528,
448
+ "c5.large": 0.085,
449
+ "c5.xlarge": 0.17,
450
+ "c5.2xlarge": 0.34,
451
+ "c5a.large": 0.077,
452
+ "c5a.xlarge": 0.154,
453
+ "c5a.2xlarge": 0.308,
454
+ "c6i.large": 0.085,
455
+ "c6i.xlarge": 0.17,
456
+ "c6i.2xlarge": 0.34,
457
+ "c6g.large": 0.068,
458
+ "c6g.xlarge": 0.136,
459
+ "c6g.2xlarge": 0.272,
460
+ "c6g.4xlarge": 0.544,
461
+ "c7i.large": 0.089,
462
+ "c7i.xlarge": 0.178,
463
+ "c7i.2xlarge": 0.357,
464
+ "c7g.large": 0.072,
465
+ "c7g.xlarge": 0.145,
466
+ "c7g.2xlarge": 0.29,
467
+ "c7g.4xlarge": 0.58,
468
+ "r5.large": 0.126,
469
+ "r5.xlarge": 0.252,
470
+ "r5.2xlarge": 0.504,
471
+ "r5a.large": 0.113,
472
+ "r5a.xlarge": 0.226,
473
+ "r5a.2xlarge": 0.452,
474
+ "r6i.large": 0.126,
475
+ "r6i.xlarge": 0.252,
476
+ "r6i.2xlarge": 0.504,
477
+ "r6g.large": 0.101,
478
+ "r6g.xlarge": 0.202,
479
+ "r6g.2xlarge": 0.403,
480
+ "r6g.4xlarge": 0.806,
481
+ "r7i.large": 0.133,
482
+ "r7i.xlarge": 0.266,
483
+ "r7i.2xlarge": 0.532,
484
+ "r7g.large": 0.107,
485
+ "r7g.xlarge": 0.214,
486
+ "r7g.2xlarge": 0.428,
487
+ "r7g.4xlarge": 0.856
488
+ };
489
+ var RDS_BASE_PRICES = {
490
+ "db.t3.micro": 0.017,
491
+ "db.t3.small": 0.034,
492
+ "db.t3.medium": 0.068,
493
+ "db.t3.large": 0.136,
494
+ "db.t4g.micro": 0.016,
495
+ "db.t4g.small": 0.032,
496
+ "db.t4g.medium": 0.065,
497
+ "db.t4g.large": 0.129,
498
+ "db.t4g.xlarge": 0.258,
499
+ "db.m5.large": 0.171,
500
+ "db.m5.xlarge": 0.342,
501
+ "db.m6g.large": 0.154,
502
+ "db.m6g.xlarge": 0.308,
503
+ "db.m6g.2xlarge": 0.616,
504
+ "db.m6i.large": 0.171,
505
+ "db.m6i.xlarge": 0.342,
506
+ "db.m6i.2xlarge": 0.684,
507
+ "db.m7g.large": 0.168,
508
+ "db.m7g.xlarge": 0.336,
509
+ "db.m7g.2xlarge": 0.672,
510
+ "db.r5.large": 0.25,
511
+ "db.r5.xlarge": 0.5,
512
+ "db.r5.2xlarge": 1,
513
+ "db.r6g.large": 0.218,
514
+ "db.r6g.xlarge": 0.437,
515
+ "db.r6g.2xlarge": 0.874,
516
+ "db.r6i.large": 0.25,
517
+ "db.r6i.xlarge": 0.5,
518
+ "db.r6i.2xlarge": 1
519
+ };
520
+ var EBS_BASE_PRICES = {
521
+ "gp3": 0.08,
522
+ "gp2": 0.1,
523
+ "io2": 0.125,
524
+ "io1": 0.125,
525
+ "st1": 0.045,
526
+ "sc1": 0.015
527
+ };
528
+ var ALB_HOURLY = 0.0225;
529
+ var ALB_LCU_HOURLY = 8e-3;
530
+ var NAT_HOURLY = 0.045;
531
+ var NAT_PER_GB = 0.045;
532
+ var EKS_HOURLY = 0.1;
533
+ var REGION_MULTIPLIERS = {
534
+ "us-east-1": 1,
535
+ "us-east-2": 1,
536
+ "us-west-1": 1.08,
537
+ "us-west-2": 1,
538
+ "eu-west-1": 1.1,
539
+ "eu-west-2": 1.12,
540
+ "eu-central-1": 1.12,
541
+ "ap-southeast-1": 1.15,
542
+ "ap-southeast-2": 1.15,
543
+ "ap-northeast-1": 1.12,
544
+ "ap-northeast-2": 1.1,
545
+ "ap-south-1": 1.08,
546
+ "sa-east-1": 1.2,
547
+ "ca-central-1": 1.08,
548
+ "eu-north-1": 1.1,
549
+ "eu-south-1": 1.12,
550
+ "me-south-1": 1.15,
551
+ "af-south-1": 1.22
552
+ };
553
+ function regionMultiplier(region) {
554
+ return REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
555
+ }
556
+ var CACHE_TTL = 86400;
557
+ var BULK_PRICING_BASE = "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws";
558
+ var AwsBulkLoader = class _AwsBulkLoader {
559
+ cache;
560
+ /**
561
+ * Deduplicates concurrent streaming CSV fetches for the same region.
562
+ * Keyed by region; value is the in-flight Promise<boolean>.
563
+ */
564
+ ec2CsvFetchInProgress = /* @__PURE__ */ new Map();
565
+ constructor(cache) {
566
+ this.cache = cache;
567
+ }
568
+ // -------------------------------------------------------------------------
569
+ // Public API
570
+ // -------------------------------------------------------------------------
571
+ async getComputePrice(instanceType, region, os = "Linux") {
572
+ const cacheKey = this.buildCacheKey("ec2", region, instanceType, os);
573
+ const cached = this.cache.get(cacheKey);
574
+ if (cached) return cached;
575
+ try {
576
+ const csvOk = await this.fetchAndCacheEc2CsvPrices(region);
577
+ if (csvOk) {
578
+ const afterCsv = this.cache.get(cacheKey);
579
+ if (afterCsv) return afterCsv;
580
+ }
581
+ } catch (err) {
582
+ logger.debug("AWS EC2 CSV streaming failed", {
583
+ region,
584
+ instanceType,
585
+ err: err instanceof Error ? err.message : String(err)
586
+ });
587
+ }
588
+ try {
589
+ const bulk = await this.fetchBulkPricing("AmazonEC2", region);
590
+ if (bulk) {
591
+ const result = this.extractEc2Price(bulk, instanceType, region, os);
592
+ if (result) {
593
+ this.cache.set(cacheKey, result, "aws", "ec2", region, CACHE_TTL);
594
+ return result;
595
+ }
596
+ }
597
+ } catch (err) {
598
+ logger.debug("AWS bulk EC2 JSON fetch failed, using fallback", {
599
+ region,
600
+ instanceType,
601
+ err: err instanceof Error ? err.message : String(err)
602
+ });
603
+ }
604
+ return this.fallbackComputePrice(instanceType, region, os, cacheKey);
605
+ }
606
+ async getDatabasePrice(instanceClass, region, engine = "MySQL") {
607
+ const cacheKey = this.buildCacheKey("rds", region, instanceClass, engine);
608
+ const cached = this.cache.get(cacheKey);
609
+ if (cached) return cached;
610
+ try {
611
+ const bulk = await this.fetchBulkPricing("AmazonRDS", region);
612
+ if (bulk) {
613
+ const result = this.extractRdsPrice(bulk, instanceClass, region, engine);
614
+ if (result) {
615
+ this.cache.set(cacheKey, result, "aws", "rds", region, CACHE_TTL);
616
+ return result;
617
+ }
618
+ }
619
+ } catch (err) {
620
+ logger.debug("AWS bulk RDS fetch failed, using fallback", {
621
+ region,
622
+ instanceClass,
623
+ err: err instanceof Error ? err.message : String(err)
624
+ });
625
+ }
626
+ return this.fallbackDatabasePrice(instanceClass, region, engine, cacheKey);
627
+ }
628
+ async getStoragePrice(volumeType, region) {
629
+ const cacheKey = this.buildCacheKey("ebs", region, volumeType);
630
+ const cached = this.cache.get(cacheKey);
631
+ if (cached) return cached;
632
+ try {
633
+ const bulk = await this.fetchBulkPricing("AmazonEC2", region);
634
+ if (bulk) {
635
+ const result = this.extractEbsPrice(bulk, volumeType, region);
636
+ if (result) {
637
+ this.cache.set(cacheKey, result, "aws", "ebs", region, CACHE_TTL);
638
+ return result;
639
+ }
640
+ }
641
+ } catch (err) {
642
+ logger.debug("AWS bulk EBS fetch failed, using fallback", {
643
+ region,
644
+ volumeType,
645
+ err: err instanceof Error ? err.message : String(err)
646
+ });
647
+ }
648
+ return this.fallbackStoragePrice(volumeType, region, cacheKey);
649
+ }
650
+ async getLoadBalancerPrice(region) {
651
+ const multiplier = regionMultiplier(region);
652
+ return {
653
+ provider: "aws",
654
+ service: "elb",
655
+ resource_type: "alb",
656
+ region,
657
+ unit: "Hrs",
658
+ price_per_unit: ALB_HOURLY * multiplier,
659
+ currency: "USD",
660
+ description: "AWS Application Load Balancer (hourly fixed charge)",
661
+ attributes: {
662
+ lcu_hourly_price: String(ALB_LCU_HOURLY * multiplier),
663
+ pricing_model: "fixed_plus_lcu"
664
+ },
665
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
666
+ };
667
+ }
668
+ async getNatGatewayPrice(region) {
669
+ const multiplier = regionMultiplier(region);
670
+ return {
671
+ provider: "aws",
672
+ service: "vpc",
673
+ resource_type: "nat-gateway",
674
+ region,
675
+ unit: "Hrs",
676
+ price_per_unit: NAT_HOURLY * multiplier,
677
+ currency: "USD",
678
+ description: "AWS NAT Gateway (hourly charge + data processing)",
679
+ attributes: {
680
+ per_gb_price: String(NAT_PER_GB * multiplier)
681
+ },
682
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
683
+ };
684
+ }
685
+ async getKubernetesPrice(region) {
686
+ const multiplier = regionMultiplier(region);
687
+ return {
688
+ provider: "aws",
689
+ service: "eks",
690
+ resource_type: "cluster",
691
+ region,
692
+ unit: "Hrs",
693
+ price_per_unit: EKS_HOURLY * multiplier,
694
+ currency: "USD",
695
+ description: "AWS EKS control plane (per cluster/hour)",
696
+ attributes: {},
697
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
698
+ };
699
+ }
700
+ // -------------------------------------------------------------------------
701
+ // Internal helpers – EC2 CSV streaming
702
+ // -------------------------------------------------------------------------
703
+ /**
704
+ * Fetch the per-region EC2 pricing CSV from the AWS Bulk Pricing endpoint,
705
+ * stream it line-by-line, parse every "Compute Instance / OnDemand / Shared"
706
+ * row for ALL operating systems, and write each extracted price into the
707
+ * SQLite cache.
708
+ *
709
+ * The CSV URL pattern is:
710
+ * https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/{region}/index.csv
711
+ *
712
+ * The first ~5 rows of the file are metadata (FormatVersion, Disclaimer,
713
+ * Publication Date, etc.). The actual header row is the first row whose
714
+ * first field is "SKU".
715
+ *
716
+ * Returns true when at least one price was extracted and cached.
717
+ */
718
+ fetchAndCacheEc2CsvPrices(region) {
719
+ const inflight = this.ec2CsvFetchInProgress.get(region);
720
+ if (inflight) return inflight;
721
+ const promise = this._doFetchAndCacheEc2CsvPrices(region).finally(() => {
722
+ this.ec2CsvFetchInProgress.delete(region);
723
+ });
724
+ this.ec2CsvFetchInProgress.set(region, promise);
725
+ return promise;
726
+ }
727
+ async _doFetchAndCacheEc2CsvPrices(region) {
728
+ const url = `${BULK_PRICING_BASE}/AmazonEC2/current/${region}/index.csv`;
729
+ logger.debug("Streaming AWS EC2 CSV pricing", { url, region });
730
+ const controller = new AbortController();
731
+ const timeoutId = setTimeout(() => controller.abort(), 12e4);
732
+ try {
733
+ const res = await fetch(url, { signal: controller.signal });
734
+ if (!res.ok) {
735
+ logger.debug("AWS EC2 CSV fetch returned non-OK status", {
736
+ region,
737
+ status: res.status
738
+ });
739
+ return false;
740
+ }
741
+ if (!res.body) {
742
+ logger.debug("AWS EC2 CSV response has no body", { region });
743
+ return false;
744
+ }
745
+ const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
746
+ let colInstanceType = -1;
747
+ let colOS = -1;
748
+ let colTenancy = -1;
749
+ let colTermType = -1;
750
+ let colCapacityStatus = -1;
751
+ let colProductFamily = -1;
752
+ let colPrice = -1;
753
+ let colUnit = -1;
754
+ let headerFound = false;
755
+ let leftover = "";
756
+ let cachedCount = 0;
757
+ const effectiveDate = (/* @__PURE__ */ new Date()).toISOString();
758
+ while (true) {
759
+ const { value, done } = await reader.read();
760
+ if (done) break;
761
+ const chunk = leftover + value;
762
+ const lines = chunk.split("\n");
763
+ leftover = lines.pop() ?? "";
764
+ for (const rawLine of lines) {
765
+ const line = rawLine.trimEnd();
766
+ if (!line) continue;
767
+ if (!headerFound) {
768
+ if (line.startsWith('"SKU"') || line.startsWith("SKU")) {
769
+ const headers = parseCsvLine(line);
770
+ for (let h = 0; h < headers.length; h++) {
771
+ const name = headers[h].trim();
772
+ if (name === "Instance Type") colInstanceType = h;
773
+ else if (name === "Operating System") colOS = h;
774
+ else if (name === "Tenancy") colTenancy = h;
775
+ else if (name === "TermType") colTermType = h;
776
+ else if (name === "Capacity Status") colCapacityStatus = h;
777
+ else if (name === "Product Family") colProductFamily = h;
778
+ else if (name === "PricePerUnit" || name === "Price Per Unit") colPrice = h;
779
+ else if (name === "Unit") colUnit = h;
780
+ }
781
+ if (colInstanceType === -1 || colOS === -1 || colPrice === -1) {
782
+ logger.debug("AWS EC2 CSV header missing required columns", {
783
+ region,
784
+ colInstanceType,
785
+ colOS,
786
+ colPrice
787
+ });
788
+ return false;
789
+ }
790
+ headerFound = true;
791
+ }
792
+ continue;
793
+ }
794
+ if (!line.includes("OnDemand") || !line.includes("Compute Instance")) {
795
+ continue;
796
+ }
797
+ const fields = parseCsvLine(line);
798
+ if (colProductFamily !== -1) {
799
+ const pf = fields[colProductFamily] ?? "";
800
+ if (pf !== "Compute Instance") continue;
801
+ }
802
+ if (colTenancy !== -1) {
803
+ const tenancy = fields[colTenancy] ?? "";
804
+ if (tenancy !== "Shared") continue;
805
+ }
806
+ if (colTermType !== -1) {
807
+ const termType = fields[colTermType] ?? "";
808
+ if (termType !== "OnDemand") continue;
809
+ }
810
+ if (colCapacityStatus !== -1) {
811
+ const cap = fields[colCapacityStatus] ?? "";
812
+ if (cap !== "Used") continue;
813
+ }
814
+ const instanceType = fields[colInstanceType] ?? "";
815
+ const os = fields[colOS] ?? "";
816
+ const rawPrice = fields[colPrice] ?? "";
817
+ const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
818
+ if (!instanceType || !os || !rawPrice) continue;
819
+ const priceValue = parseFloat(rawPrice);
820
+ if (!isFinite(priceValue) || priceValue <= 0) continue;
821
+ const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
822
+ const normalized = {
823
+ provider: "aws",
824
+ service: "ec2",
825
+ resource_type: instanceType,
826
+ region,
827
+ unit,
828
+ price_per_unit: priceValue,
829
+ currency: "USD",
830
+ description: `AWS EC2 ${instanceType} (${os}) on-demand`,
831
+ attributes: {
832
+ instance_type: instanceType,
833
+ operating_system: os,
834
+ tenancy: "Shared",
835
+ pricing_source: "live"
836
+ },
837
+ effective_date: effectiveDate
838
+ };
839
+ this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, CACHE_TTL);
840
+ cachedCount++;
841
+ }
842
+ }
843
+ if (leftover.trim() && headerFound) {
844
+ const line = leftover.trimEnd();
845
+ if (line.includes("OnDemand") && line.includes("Compute Instance")) {
846
+ const fields = parseCsvLine(line);
847
+ const instanceType = fields[colInstanceType] ?? "";
848
+ const os = fields[colOS] ?? "";
849
+ const rawPrice = fields[colPrice] ?? "";
850
+ const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
851
+ const priceValue = parseFloat(rawPrice);
852
+ if (instanceType && os && isFinite(priceValue) && priceValue > 0) {
853
+ const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
854
+ this.cache.set(
855
+ csvCacheKey,
856
+ {
857
+ provider: "aws",
858
+ service: "ec2",
859
+ resource_type: instanceType,
860
+ region,
861
+ unit,
862
+ price_per_unit: priceValue,
863
+ currency: "USD",
864
+ description: `AWS EC2 ${instanceType} (${os}) on-demand`,
865
+ attributes: {
866
+ instance_type: instanceType,
867
+ operating_system: os,
868
+ tenancy: "Shared",
869
+ pricing_source: "live"
870
+ },
871
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
872
+ },
873
+ "aws",
874
+ "ec2",
875
+ region,
876
+ CACHE_TTL
877
+ );
878
+ cachedCount++;
879
+ }
880
+ }
881
+ }
882
+ logger.debug("AWS EC2 CSV streaming complete", { region, cachedCount });
883
+ return cachedCount > 0;
884
+ } catch (err) {
885
+ if (err?.name === "AbortError") {
886
+ logger.debug("AWS EC2 CSV streaming timed out", { region });
887
+ } else {
888
+ logger.debug("AWS EC2 CSV streaming error", {
889
+ region,
890
+ err: err instanceof Error ? err.message : String(err)
891
+ });
892
+ }
893
+ return false;
894
+ } finally {
895
+ clearTimeout(timeoutId);
896
+ }
897
+ }
898
+ // -------------------------------------------------------------------------
899
+ // Internal helpers – live API fetch
900
+ // -------------------------------------------------------------------------
901
+ async fetchBulkPricing(service, region) {
902
+ const url = `${BULK_PRICING_BASE}/${service}/current/${region}/index.json`;
903
+ logger.debug("Fetching AWS bulk pricing", { url });
904
+ const res = await fetch(url, {
905
+ signal: AbortSignal.timeout(3e4)
906
+ });
907
+ if (!res.ok) {
908
+ throw new Error(`HTTP ${res.status} from ${url}`);
909
+ }
910
+ return res.json();
911
+ }
912
+ buildCacheKey(service, region, ...parts) {
913
+ return ["aws", service, region, ...parts].map((p) => p.toLowerCase()).join("/");
914
+ }
915
+ // -------------------------------------------------------------------------
916
+ // Internal helpers – extract prices from bulk JSON
917
+ // -------------------------------------------------------------------------
918
+ extractEc2Price(bulk, instanceType, region, os) {
919
+ const products = bulk?.products ?? {};
920
+ const onDemand = bulk?.terms?.OnDemand ?? {};
921
+ for (const [sku, product] of Object.entries(products)) {
922
+ const attrs = product?.attributes ?? {};
923
+ if (attrs.instanceType === instanceType && (attrs.operatingSystem ?? "").toLowerCase() === os.toLowerCase() && attrs.tenancy === "Shared" && attrs.capacitystatus === "Used") {
924
+ const priceTerms = onDemand[sku];
925
+ if (priceTerms) {
926
+ return normalizeAwsCompute(product, { terms: { OnDemand: { [sku]: priceTerms } } }, region);
927
+ }
928
+ }
929
+ }
930
+ return null;
931
+ }
932
+ extractRdsPrice(bulk, instanceClass, region, engine) {
933
+ const products = bulk?.products ?? {};
934
+ const onDemand = bulk?.terms?.OnDemand ?? {};
935
+ for (const [sku, product] of Object.entries(products)) {
936
+ const attrs = product?.attributes ?? {};
937
+ if (attrs.instanceType === instanceClass && (attrs.databaseEngine ?? "").toLowerCase().includes(engine.toLowerCase()) && attrs.deploymentOption === "Single-AZ") {
938
+ const priceTerms = onDemand[sku];
939
+ if (priceTerms) {
940
+ return normalizeAwsDatabase(product, { terms: { OnDemand: { [sku]: priceTerms } } }, region);
941
+ }
942
+ }
943
+ }
944
+ return null;
945
+ }
946
+ extractEbsPrice(bulk, volumeType, region) {
947
+ const products = bulk?.products ?? {};
948
+ const onDemand = bulk?.terms?.OnDemand ?? {};
949
+ for (const [sku, product] of Object.entries(products)) {
950
+ const attrs = product?.attributes ?? {};
951
+ const apiName = (attrs.volumeApiName ?? "").toLowerCase();
952
+ if (product?.productFamily === "Storage" && (apiName === volumeType.toLowerCase() || (attrs.volumeType ?? "").toLowerCase() === volumeType.toLowerCase())) {
953
+ const priceTerms = onDemand[sku];
954
+ if (priceTerms) {
955
+ return normalizeAwsStorage(product, { terms: { OnDemand: { [sku]: priceTerms } } }, region);
956
+ }
957
+ }
958
+ }
959
+ return null;
960
+ }
961
+ // -------------------------------------------------------------------------
962
+ // Internal helpers – size interpolation
963
+ // -------------------------------------------------------------------------
964
+ /**
965
+ * AWS instance sizes follow a predictable doubling pattern:
966
+ * nano → micro → small → medium → large → xlarge → 2xlarge → 4xlarge → 8xlarge → ...
967
+ * Each step roughly doubles the price. Given a known price at one size,
968
+ * we can estimate the price for a missing size in the same family.
969
+ */
970
+ static SIZE_ORDER = [
971
+ "nano",
972
+ "micro",
973
+ "small",
974
+ "medium",
975
+ "large",
976
+ "xlarge",
977
+ "2xlarge",
978
+ "4xlarge",
979
+ "8xlarge",
980
+ "12xlarge",
981
+ "16xlarge",
982
+ "24xlarge",
983
+ "32xlarge",
984
+ "48xlarge"
985
+ ];
986
+ interpolatePrice(requestedType, table) {
987
+ const lower = requestedType.toLowerCase();
988
+ const lastDot = lower.lastIndexOf(".");
989
+ if (lastDot === -1) return void 0;
990
+ const family = lower.slice(0, lastDot);
991
+ const targetSize = lower.slice(lastDot + 1);
992
+ const targetIdx = _AwsBulkLoader.SIZE_ORDER.indexOf(targetSize);
993
+ if (targetIdx === -1) return void 0;
994
+ let bestKey;
995
+ let bestIdx = -1;
996
+ let bestDistance = Infinity;
997
+ for (const key of Object.keys(table)) {
998
+ const keyLastDot = key.lastIndexOf(".");
999
+ if (keyLastDot === -1) continue;
1000
+ const keyFamily = key.slice(0, keyLastDot);
1001
+ const keySize = key.slice(keyLastDot + 1);
1002
+ if (keyFamily !== family) continue;
1003
+ const keyIdx = _AwsBulkLoader.SIZE_ORDER.indexOf(keySize);
1004
+ if (keyIdx === -1) continue;
1005
+ const distance = Math.abs(keyIdx - targetIdx);
1006
+ if (distance < bestDistance) {
1007
+ bestDistance = distance;
1008
+ bestIdx = keyIdx;
1009
+ bestKey = key;
1010
+ }
1011
+ }
1012
+ if (bestKey === void 0 || bestIdx === -1) return void 0;
1013
+ const knownPrice = table[bestKey];
1014
+ const steps = targetIdx - bestIdx;
1015
+ return knownPrice * Math.pow(2, steps);
1016
+ }
1017
+ // -------------------------------------------------------------------------
1018
+ // Internal helpers – fallback hardcoded prices
1019
+ // -------------------------------------------------------------------------
1020
+ fallbackComputePrice(instanceType, region, os, cacheKey) {
1021
+ let basePrice = EC2_BASE_PRICES[instanceType.toLowerCase()];
1022
+ if (basePrice === void 0) {
1023
+ basePrice = this.interpolatePrice(instanceType, EC2_BASE_PRICES) ?? void 0;
1024
+ }
1025
+ if (basePrice === void 0) {
1026
+ logger.warn("No fallback price found for EC2 instance type", { instanceType });
1027
+ return null;
1028
+ }
1029
+ const multiplier = regionMultiplier(region);
1030
+ const result = {
1031
+ provider: "aws",
1032
+ service: "ec2",
1033
+ resource_type: instanceType,
1034
+ region,
1035
+ unit: "Hrs",
1036
+ price_per_unit: basePrice * multiplier,
1037
+ currency: "USD",
1038
+ description: `AWS EC2 ${instanceType} (${os}) \u2013 fallback pricing`,
1039
+ attributes: {
1040
+ instance_type: instanceType,
1041
+ operating_system: os,
1042
+ tenancy: "Shared",
1043
+ pricing_source: "fallback"
1044
+ },
1045
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1046
+ };
1047
+ this.cache.set(cacheKey, result, "aws", "ec2", region, CACHE_TTL);
1048
+ return result;
1049
+ }
1050
+ fallbackDatabasePrice(instanceClass, region, engine, cacheKey) {
1051
+ let basePrice = RDS_BASE_PRICES[instanceClass.toLowerCase()];
1052
+ if (basePrice === void 0) {
1053
+ basePrice = this.interpolatePrice(instanceClass, RDS_BASE_PRICES) ?? void 0;
1054
+ }
1055
+ if (basePrice === void 0) {
1056
+ logger.warn("No fallback price found for RDS instance class", { instanceClass });
1057
+ return null;
1058
+ }
1059
+ const multiplier = regionMultiplier(region);
1060
+ const result = {
1061
+ provider: "aws",
1062
+ service: "rds",
1063
+ resource_type: instanceClass,
1064
+ region,
1065
+ unit: "Hrs",
1066
+ price_per_unit: basePrice * multiplier,
1067
+ currency: "USD",
1068
+ description: `AWS RDS ${instanceClass} (${engine}) \u2013 fallback pricing`,
1069
+ attributes: {
1070
+ instance_type: instanceClass,
1071
+ database_engine: engine,
1072
+ deployment_option: "Single-AZ",
1073
+ pricing_source: "fallback"
1074
+ },
1075
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1076
+ };
1077
+ this.cache.set(cacheKey, result, "aws", "rds", region, CACHE_TTL);
1078
+ return result;
1079
+ }
1080
+ fallbackStoragePrice(volumeType, region, cacheKey) {
1081
+ const basePrice = EBS_BASE_PRICES[volumeType.toLowerCase()];
1082
+ if (basePrice === void 0) {
1083
+ logger.warn("No fallback price found for EBS volume type", { volumeType });
1084
+ return null;
1085
+ }
1086
+ const multiplier = regionMultiplier(region);
1087
+ const result = {
1088
+ provider: "aws",
1089
+ service: "ebs",
1090
+ resource_type: volumeType,
1091
+ region,
1092
+ unit: "GB-Mo",
1093
+ price_per_unit: basePrice * multiplier,
1094
+ currency: "USD",
1095
+ description: `AWS EBS ${volumeType} volume \u2013 fallback pricing`,
1096
+ attributes: {
1097
+ volume_type: volumeType,
1098
+ pricing_source: "fallback"
1099
+ },
1100
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1101
+ };
1102
+ this.cache.set(cacheKey, result, "aws", "ebs", region, CACHE_TTL);
1103
+ return result;
1104
+ }
1105
+ };
1106
+
1107
+ // src/pricing/azure/azure-normalizer.ts
1108
+ function normalizeAzureCompute(item) {
1109
+ return {
1110
+ provider: "azure",
1111
+ service: "virtual-machines",
1112
+ resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1113
+ region: item.armRegionName ?? item.location ?? "",
1114
+ unit: item.unitOfMeasure ?? "1 Hour",
1115
+ price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1116
+ currency: item.currencyCode ?? "USD",
1117
+ tier: item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
1118
+ description: item.productName ?? void 0,
1119
+ attributes: {
1120
+ sku_name: item.skuName ?? "",
1121
+ arm_sku_name: item.armSkuName ?? "",
1122
+ service_name: item.serviceName ?? "Virtual Machines",
1123
+ product_name: item.productName ?? "",
1124
+ meter_name: item.meterName ?? "",
1125
+ pricing_source: "azure-retail-api"
1126
+ },
1127
+ effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1128
+ };
1129
+ }
1130
+ function normalizeAzureDatabase(item) {
1131
+ return {
1132
+ provider: "azure",
1133
+ service: "azure-database",
1134
+ resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1135
+ region: item.armRegionName ?? item.location ?? "",
1136
+ unit: item.unitOfMeasure ?? "1 Hour",
1137
+ price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1138
+ currency: item.currencyCode ?? "USD",
1139
+ description: item.productName ?? void 0,
1140
+ attributes: {
1141
+ sku_name: item.skuName ?? "",
1142
+ service_name: item.serviceName ?? "",
1143
+ product_name: item.productName ?? "",
1144
+ meter_name: item.meterName ?? "",
1145
+ pricing_source: "azure-retail-api"
1146
+ },
1147
+ effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1148
+ };
1149
+ }
1150
+ function normalizeAzureStorage(item) {
1151
+ return {
1152
+ provider: "azure",
1153
+ service: "managed-disks",
1154
+ resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1155
+ region: item.armRegionName ?? item.location ?? "",
1156
+ unit: item.unitOfMeasure ?? "1 GiB/Month",
1157
+ price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1158
+ currency: item.currencyCode ?? "USD",
1159
+ description: item.productName ?? void 0,
1160
+ attributes: {
1161
+ sku_name: item.skuName ?? "",
1162
+ service_name: item.serviceName ?? "",
1163
+ product_name: item.productName ?? "",
1164
+ meter_name: item.meterName ?? "",
1165
+ pricing_source: "azure-retail-api"
1166
+ },
1167
+ effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1168
+ };
1169
+ }
1170
+
1171
+ // src/pricing/azure/retail-client.ts
1172
+ var VM_BASE_PRICES = {
1173
+ "standard_b1s": 0.0104,
1174
+ "standard_b1ms": 0.0207,
1175
+ "standard_b2s": 0.0416,
1176
+ "standard_b2ms": 0.0832,
1177
+ "standard_b4ms": 0.166,
1178
+ "standard_b8ms": 0.333,
1179
+ "standard_b12ms": 0.499,
1180
+ "standard_b16ms": 0.666,
1181
+ "standard_d2s_v4": 0.096,
1182
+ "standard_d4s_v4": 0.192,
1183
+ "standard_d8s_v4": 0.384,
1184
+ "standard_d2s_v5": 0.096,
1185
+ "standard_d4s_v5": 0.192,
1186
+ "standard_d8s_v5": 0.384,
1187
+ "standard_d16s_v5": 0.768,
1188
+ "standard_d32s_v5": 1.536,
1189
+ "standard_d2ds_v5": 0.113,
1190
+ "standard_d4ds_v5": 0.226,
1191
+ "standard_d8ds_v5": 0.452,
1192
+ "standard_d2ps_v5": 0.077,
1193
+ "standard_d4ps_v5": 0.154,
1194
+ "standard_e2s_v5": 0.126,
1195
+ "standard_e4s_v5": 0.252,
1196
+ "standard_e8s_v5": 0.504,
1197
+ "standard_e16s_v5": 1.008,
1198
+ "standard_e32s_v5": 2.016,
1199
+ "standard_e2ds_v5": 0.144,
1200
+ "standard_e4ds_v5": 0.288,
1201
+ "standard_f2s_v2": 0.085,
1202
+ "standard_f4s_v2": 0.17,
1203
+ "standard_f8s_v2": 0.34,
1204
+ "standard_f16s_v2": 0.68,
1205
+ "standard_f32s_v2": 1.36,
1206
+ "standard_l8s_v3": 0.624,
1207
+ "standard_l16s_v3": 1.248,
1208
+ "standard_nc4as_t4_v3": 0.526,
1209
+ "standard_nc8as_t4_v3": 0.752,
1210
+ "standard_e2ps_v5": 0.101,
1211
+ "standard_e4ps_v5": 0.202,
1212
+ "standard_e8ps_v5": 0.403
1213
+ };
1214
+ var DISK_BASE_PRICES = {
1215
+ "premium_lrs": 0.132,
1216
+ "standard_lrs": 0.04,
1217
+ "standardssd_lrs": 0.075,
1218
+ "ultrassd_lrs": 0.12,
1219
+ "premium_lrs_p10": 52e-4,
1220
+ "premium_lrs_p20": 0.01,
1221
+ "premium_lrs_p30": 0.0192,
1222
+ "premium_lrs_p40": 0.0373
1223
+ };
1224
+ var DB_BASE_PRICES = {
1225
+ "burstable_standard_b1ms": 0.044,
1226
+ "burstable_standard_b2s": 0.088,
1227
+ "burstable_standard_b4ms": 0.176,
1228
+ "gp_standard_d2s_v3": 0.124,
1229
+ "gp_standard_d4s_v3": 0.248,
1230
+ "gp_standard_d8s_v3": 0.496,
1231
+ "gp_standard_d16s_v3": 0.992,
1232
+ "gp_standard_d2ds_v4": 0.124,
1233
+ "gp_standard_d4ds_v4": 0.248,
1234
+ "gp_standard_d8ds_v4": 0.496,
1235
+ "gp_standard_d16ds_v4": 0.992,
1236
+ "mo_standard_e2ds_v4": 0.17,
1237
+ "mo_standard_e4ds_v4": 0.341,
1238
+ "mo_standard_e8ds_v4": 0.682
1239
+ };
1240
+ var ALB_HOURLY2 = 0.025;
1241
+ var ALB_RULE_HOURLY = 5e-3;
1242
+ var NAT_HOURLY2 = 0.045;
1243
+ var NAT_PER_GB2 = 0.045;
1244
+ var AKS_HOURLY = 0.1;
1245
+ var REGION_MULTIPLIERS2 = {
1246
+ eastus: 1,
1247
+ eastus2: 1,
1248
+ westus: 1.05,
1249
+ westus2: 1,
1250
+ westus3: 1.02,
1251
+ centralus: 1,
1252
+ northeurope: 1.08,
1253
+ westeurope: 1.08,
1254
+ uksouth: 1.08,
1255
+ ukwest: 1.12,
1256
+ southeastasia: 1.12,
1257
+ eastasia: 1.12,
1258
+ japaneast: 1.1,
1259
+ japanwest: 1.12,
1260
+ australiaeast: 1.15,
1261
+ brazilsouth: 1.25,
1262
+ canadacentral: 1.05,
1263
+ southafricanorth: 1.15,
1264
+ germanywestcentral: 1.1,
1265
+ centralindia: 1.08,
1266
+ koreacentral: 1.1,
1267
+ swedencentral: 1.08,
1268
+ italynorth: 1.1,
1269
+ uaenorth: 1.12
1270
+ };
1271
+ function regionMultiplier2(region) {
1272
+ return REGION_MULTIPLIERS2[region.toLowerCase()] ?? 1;
1273
+ }
1274
+ var CACHE_TTL2 = 86400;
1275
+ var RETAIL_API_BASE = "https://prices.azure.com/api/retail/prices";
1276
+ var AzureRetailClient = class {
1277
+ cache;
1278
+ constructor(cache) {
1279
+ this.cache = cache;
1280
+ }
1281
+ // -------------------------------------------------------------------------
1282
+ // Public API
1283
+ // -------------------------------------------------------------------------
1284
+ async getComputePrice(vmSize, region, os = "linux") {
1285
+ const cacheKey = this.buildCacheKey("vm", region, vmSize, os);
1286
+ const cached = this.cache.get(cacheKey);
1287
+ if (cached) return cached;
1288
+ try {
1289
+ const armRegion = region.toLowerCase().replace(/\s+/g, "");
1290
+ const filter = this.buildODataFilter("Virtual Machines", armRegion, vmSize, true);
1291
+ const items = await this.queryPricing(filter);
1292
+ const match = this.pickVmItem(items, vmSize, os);
1293
+ if (match) {
1294
+ const result = normalizeAzureCompute(match);
1295
+ this.cache.set(cacheKey, result, "azure", "vm", region, CACHE_TTL2);
1296
+ return result;
1297
+ }
1298
+ } catch (err) {
1299
+ logger.debug("Azure Retail API VM fetch failed, using fallback", {
1300
+ region,
1301
+ vmSize,
1302
+ err: err instanceof Error ? err.message : String(err)
1303
+ });
1304
+ }
1305
+ return this.fallbackComputePrice(vmSize, region, os, cacheKey);
1306
+ }
1307
+ async getDatabasePrice(tier, region, engine = "PostgreSQL") {
1308
+ const cacheKey = this.buildCacheKey("db", region, tier, engine);
1309
+ const cached = this.cache.get(cacheKey);
1310
+ if (cached) return cached;
1311
+ try {
1312
+ const armRegion = region.toLowerCase().replace(/\s+/g, "");
1313
+ const serviceName = `Azure Database for ${engine}`;
1314
+ const filter = this.buildODataFilter(serviceName, armRegion, tier);
1315
+ const items = await this.queryPricing(filter);
1316
+ if (items.length > 0) {
1317
+ const result = normalizeAzureDatabase(items[0]);
1318
+ this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
1319
+ return result;
1320
+ }
1321
+ } catch (err) {
1322
+ logger.debug("Azure Retail API DB fetch failed, using fallback", {
1323
+ region,
1324
+ tier,
1325
+ err: err instanceof Error ? err.message : String(err)
1326
+ });
1327
+ }
1328
+ return this.fallbackDatabasePrice(tier, region, engine, cacheKey);
1329
+ }
1330
+ async getStoragePrice(diskType, region) {
1331
+ const cacheKey = this.buildCacheKey("disk", region, diskType);
1332
+ const cached = this.cache.get(cacheKey);
1333
+ if (cached) return cached;
1334
+ try {
1335
+ const armRegion = region.toLowerCase().replace(/\s+/g, "");
1336
+ const filter = this.buildODataFilter("Storage", armRegion, diskType);
1337
+ const items = await this.queryPricing(filter);
1338
+ if (items.length > 0) {
1339
+ const result = normalizeAzureStorage(items[0]);
1340
+ this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
1341
+ return result;
1342
+ }
1343
+ } catch (err) {
1344
+ logger.debug("Azure Retail API storage fetch failed, using fallback", {
1345
+ region,
1346
+ diskType,
1347
+ err: err instanceof Error ? err.message : String(err)
1348
+ });
1349
+ }
1350
+ return this.fallbackStoragePrice(diskType, region, cacheKey);
1351
+ }
1352
+ async getLoadBalancerPrice(region) {
1353
+ const multiplier = regionMultiplier2(region);
1354
+ return {
1355
+ provider: "azure",
1356
+ service: "load-balancer",
1357
+ resource_type: "standard",
1358
+ region,
1359
+ unit: "1 Hour",
1360
+ price_per_unit: ALB_HOURLY2 * multiplier,
1361
+ currency: "USD",
1362
+ description: "Azure Load Balancer Standard (hourly + per rule)",
1363
+ attributes: {
1364
+ rule_hourly_price: String(ALB_RULE_HOURLY * multiplier),
1365
+ pricing_model: "fixed_plus_rules"
1366
+ },
1367
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1368
+ };
1369
+ }
1370
+ async getNatGatewayPrice(region) {
1371
+ const multiplier = regionMultiplier2(region);
1372
+ return {
1373
+ provider: "azure",
1374
+ service: "nat-gateway",
1375
+ resource_type: "nat-gateway",
1376
+ region,
1377
+ unit: "1 Hour",
1378
+ price_per_unit: NAT_HOURLY2 * multiplier,
1379
+ currency: "USD",
1380
+ description: "Azure NAT Gateway (hourly + data processed)",
1381
+ attributes: {
1382
+ per_gb_price: String(NAT_PER_GB2 * multiplier)
1383
+ },
1384
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1385
+ };
1386
+ }
1387
+ async getKubernetesPrice(region) {
1388
+ const multiplier = regionMultiplier2(region);
1389
+ return {
1390
+ provider: "azure",
1391
+ service: "aks",
1392
+ resource_type: "cluster",
1393
+ region,
1394
+ unit: "1 Hour",
1395
+ price_per_unit: AKS_HOURLY * multiplier,
1396
+ currency: "USD",
1397
+ description: "Azure Kubernetes Service (uptime SLA tier, per cluster/hour)",
1398
+ attributes: {},
1399
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1400
+ };
1401
+ }
1402
+ // -------------------------------------------------------------------------
1403
+ // Internal helpers – live API
1404
+ // -------------------------------------------------------------------------
1405
+ async queryPricing(filter) {
1406
+ const allItems = [];
1407
+ let url = `${RETAIL_API_BASE}?api-version=2023-01-01-preview&$filter=${encodeURIComponent(filter)}`;
1408
+ logger.debug("Querying Azure Retail Prices API", { filter });
1409
+ let pages = 0;
1410
+ while (url && pages < 10) {
1411
+ const res = await fetch(url, {
1412
+ signal: AbortSignal.timeout(3e4)
1413
+ });
1414
+ if (!res.ok) {
1415
+ throw new Error(`HTTP ${res.status} from Azure Retail API`);
1416
+ }
1417
+ const json = await res.json();
1418
+ allItems.push(...json?.Items ?? []);
1419
+ url = json?.NextPageLink ?? null;
1420
+ pages++;
1421
+ }
1422
+ return allItems;
1423
+ }
1424
+ buildODataFilter(service, armRegion, skuName, exactArmSku) {
1425
+ let filter = `serviceName eq '${service}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption'`;
1426
+ if (skuName) {
1427
+ if (exactArmSku) {
1428
+ filter += ` and armSkuName eq '${skuName}'`;
1429
+ } else {
1430
+ filter += ` and contains(skuName, '${skuName}')`;
1431
+ }
1432
+ }
1433
+ return filter;
1434
+ }
1435
+ buildCacheKey(service, region, ...parts) {
1436
+ return ["azure", service, region, ...parts].map((p) => p.toLowerCase()).join("/");
1437
+ }
1438
+ /**
1439
+ * From the API response items, pick the item that best matches the desired
1440
+ * VM size and OS. Prefers exact skuName matches and the correct OS suffix
1441
+ * ("Windows" vs everything else being Linux).
1442
+ */
1443
+ pickVmItem(items, vmSize, os) {
1444
+ if (items.length === 0) return null;
1445
+ const vmLower = vmSize.toLowerCase();
1446
+ const isWindows = os.toLowerCase().includes("windows");
1447
+ for (const item of items) {
1448
+ const skuLower = (item.skuName ?? "").toLowerCase();
1449
+ const hasWindows = skuLower.includes("windows");
1450
+ if (skuLower.includes(vmLower) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
1451
+ return item;
1452
+ }
1453
+ }
1454
+ return items[0];
1455
+ }
1456
+ // -------------------------------------------------------------------------
1457
+ // Internal helpers – size interpolation
1458
+ // -------------------------------------------------------------------------
1459
+ /**
1460
+ * Azure VM sizes embed vCPU count as a number: Standard_D2s_v5 → 2 vCPUs.
1461
+ * Doubling the number roughly doubles the price. Find the nearest known
1462
+ * size in the same family/suffix and scale proportionally.
1463
+ */
1464
+ interpolateVmPrice(vmSize, table) {
1465
+ const key = vmSize.toLowerCase().replace(/\s+/g, "_");
1466
+ const match = key.match(/^(.+?)(\d+)(.*?)$/);
1467
+ if (!match) return void 0;
1468
+ const [, prefix, numStr, suffix] = match;
1469
+ const targetNum = parseInt(numStr, 10);
1470
+ if (isNaN(targetNum) || targetNum === 0) return void 0;
1471
+ let bestKey;
1472
+ let bestNum = 0;
1473
+ let bestDistance = Infinity;
1474
+ for (const candidate of Object.keys(table)) {
1475
+ const candMatch = candidate.match(/^(.+?)(\d+)(.*?)$/);
1476
+ if (!candMatch) continue;
1477
+ const [, cPrefix, cNumStr, cSuffix] = candMatch;
1478
+ if (cPrefix !== prefix || cSuffix !== suffix) continue;
1479
+ const cNum = parseInt(cNumStr, 10);
1480
+ if (isNaN(cNum) || cNum === 0) continue;
1481
+ const distance = Math.abs(cNum - targetNum);
1482
+ if (distance < bestDistance) {
1483
+ bestDistance = distance;
1484
+ bestNum = cNum;
1485
+ bestKey = candidate;
1486
+ }
1487
+ }
1488
+ if (bestKey === void 0 || bestNum === 0) return void 0;
1489
+ return table[bestKey] * (targetNum / bestNum);
1490
+ }
1491
+ /**
1492
+ * Azure DB tier names embed a vCPU number similarly to VM sizes.
1493
+ * e.g. "gp_standard_d2s_v3" → "gp_standard_d4s_v3" doubles.
1494
+ */
1495
+ interpolateDbPrice(tier, table) {
1496
+ const key = tier.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
1497
+ const match = key.match(/^(.+?)(\d+)(.*?)$/);
1498
+ if (!match) return void 0;
1499
+ const [, prefix, numStr, suffix] = match;
1500
+ const targetNum = parseInt(numStr, 10);
1501
+ if (isNaN(targetNum) || targetNum === 0) return void 0;
1502
+ let bestKey;
1503
+ let bestNum = 0;
1504
+ let bestDistance = Infinity;
1505
+ for (const candidate of Object.keys(table)) {
1506
+ const candMatch = candidate.match(/^(.+?)(\d+)(.*?)$/);
1507
+ if (!candMatch) continue;
1508
+ const [, cPrefix, cNumStr, cSuffix] = candMatch;
1509
+ if (cPrefix !== prefix || cSuffix !== suffix) continue;
1510
+ const cNum = parseInt(cNumStr, 10);
1511
+ if (isNaN(cNum) || cNum === 0) continue;
1512
+ const distance = Math.abs(cNum - targetNum);
1513
+ if (distance < bestDistance) {
1514
+ bestDistance = distance;
1515
+ bestNum = cNum;
1516
+ bestKey = candidate;
1517
+ }
1518
+ }
1519
+ if (bestKey === void 0 || bestNum === 0) return void 0;
1520
+ return table[bestKey] * (targetNum / bestNum);
1521
+ }
1522
+ // -------------------------------------------------------------------------
1523
+ // Internal helpers – fallback hardcoded prices
1524
+ // -------------------------------------------------------------------------
1525
+ fallbackComputePrice(vmSize, region, os, cacheKey) {
1526
+ const key = vmSize.toLowerCase().replace(/\s+/g, "_");
1527
+ let basePrice = VM_BASE_PRICES[key];
1528
+ if (basePrice === void 0) {
1529
+ basePrice = this.interpolateVmPrice(vmSize, VM_BASE_PRICES) ?? void 0;
1530
+ }
1531
+ if (basePrice === void 0) {
1532
+ logger.warn("No fallback price found for Azure VM size", { vmSize });
1533
+ return null;
1534
+ }
1535
+ const multiplier = regionMultiplier2(region);
1536
+ const result = {
1537
+ provider: "azure",
1538
+ service: "virtual-machines",
1539
+ resource_type: vmSize,
1540
+ region,
1541
+ unit: "1 Hour",
1542
+ price_per_unit: basePrice * multiplier,
1543
+ currency: "USD",
1544
+ description: `Azure VM ${vmSize} (${os}) \u2013 fallback pricing`,
1545
+ attributes: {
1546
+ sku_name: vmSize,
1547
+ operating_system: os,
1548
+ pricing_source: "fallback"
1549
+ },
1550
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1551
+ };
1552
+ this.cache.set(cacheKey, result, "azure", "vm", region, CACHE_TTL2);
1553
+ return result;
1554
+ }
1555
+ fallbackDatabasePrice(tier, region, engine, cacheKey) {
1556
+ const key = tier.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
1557
+ let basePrice = DB_BASE_PRICES[key];
1558
+ if (basePrice === void 0) {
1559
+ basePrice = this.interpolateDbPrice(tier, DB_BASE_PRICES) ?? void 0;
1560
+ }
1561
+ if (basePrice === void 0) {
1562
+ logger.warn("No fallback price found for Azure DB tier", { tier });
1563
+ return null;
1564
+ }
1565
+ const multiplier = regionMultiplier2(region);
1566
+ const result = {
1567
+ provider: "azure",
1568
+ service: "azure-database",
1569
+ resource_type: tier,
1570
+ region,
1571
+ unit: "1 Hour",
1572
+ price_per_unit: basePrice * multiplier,
1573
+ currency: "USD",
1574
+ description: `Azure Database for ${engine} ${tier} \u2013 fallback pricing`,
1575
+ attributes: {
1576
+ tier,
1577
+ engine,
1578
+ pricing_source: "fallback"
1579
+ },
1580
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1581
+ };
1582
+ this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
1583
+ return result;
1584
+ }
1585
+ fallbackStoragePrice(diskType, region, cacheKey) {
1586
+ const key = diskType.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
1587
+ const basePrice = DISK_BASE_PRICES[key];
1588
+ if (basePrice === void 0) {
1589
+ logger.warn("No fallback price found for Azure disk type", { diskType });
1590
+ return null;
1591
+ }
1592
+ const multiplier = regionMultiplier2(region);
1593
+ const result = {
1594
+ provider: "azure",
1595
+ service: "managed-disks",
1596
+ resource_type: diskType,
1597
+ region,
1598
+ unit: "1 GiB/Month",
1599
+ price_per_unit: basePrice * multiplier,
1600
+ currency: "USD",
1601
+ description: `Azure Managed Disk ${diskType} \u2013 fallback pricing`,
1602
+ attributes: {
1603
+ disk_type: diskType,
1604
+ pricing_source: "fallback"
1605
+ },
1606
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1607
+ };
1608
+ this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
1609
+ return result;
1610
+ }
1611
+ };
1612
+
1613
+ // src/pricing/gcp/gcp-normalizer.ts
1614
+ function normalizeGcpCompute(machineType, hourlyPrice, region) {
1615
+ return {
1616
+ provider: "gcp",
1617
+ service: "compute-engine",
1618
+ resource_type: machineType,
1619
+ region,
1620
+ unit: "h",
1621
+ price_per_unit: hourlyPrice,
1622
+ currency: "USD",
1623
+ description: `GCP Compute Engine ${machineType}`,
1624
+ attributes: {
1625
+ machine_type: machineType,
1626
+ pricing_source: "bundled"
1627
+ },
1628
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1629
+ };
1630
+ }
1631
+ function normalizeGcpDatabase(tier, hourlyPrice, region) {
1632
+ return {
1633
+ provider: "gcp",
1634
+ service: "cloud-sql",
1635
+ resource_type: tier,
1636
+ region,
1637
+ unit: "h",
1638
+ price_per_unit: hourlyPrice,
1639
+ currency: "USD",
1640
+ description: `GCP Cloud SQL ${tier}`,
1641
+ attributes: {
1642
+ tier,
1643
+ pricing_source: "bundled"
1644
+ },
1645
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1646
+ };
1647
+ }
1648
+ function normalizeGcpStorage(storageClass, pricePerGb, region) {
1649
+ return {
1650
+ provider: "gcp",
1651
+ service: "cloud-storage",
1652
+ resource_type: storageClass,
1653
+ region,
1654
+ unit: "GiBy.mo",
1655
+ price_per_unit: pricePerGb,
1656
+ currency: "USD",
1657
+ description: `GCP Cloud Storage ${storageClass}`,
1658
+ attributes: {
1659
+ storage_class: storageClass,
1660
+ pricing_source: "bundled"
1661
+ },
1662
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1663
+ };
1664
+ }
1665
+ function normalizeGcpDisk(diskType, pricePerGb, region) {
1666
+ return {
1667
+ provider: "gcp",
1668
+ service: "persistent-disk",
1669
+ resource_type: diskType,
1670
+ region,
1671
+ unit: "GiBy.mo",
1672
+ price_per_unit: pricePerGb,
1673
+ currency: "USD",
1674
+ description: `GCP Persistent Disk ${diskType}`,
1675
+ attributes: {
1676
+ disk_type: diskType,
1677
+ pricing_source: "bundled"
1678
+ },
1679
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1680
+ };
1681
+ }
1682
+
1683
+ // src/pricing/gcp/bundled-loader.ts
1684
+ var LB_FORWARDING_RULE_HOURLY = 0.025;
1685
+ var LB_PER_GB = 8e-3;
1686
+ var NAT_HOURLY3 = 0.044;
1687
+ var NAT_PER_GB3 = 0.045;
1688
+ var GKE_STANDARD_HOURLY = 0.1;
1689
+ var GKE_AUTOPILOT_VCPU_HOURLY = 0.0445;
1690
+ var GcpBundledLoader = class {
1691
+ // -------------------------------------------------------------------------
1692
+ // Public API
1693
+ // -------------------------------------------------------------------------
1694
+ async getComputePrice(machineType, region) {
1695
+ try {
1696
+ const pricing = getGcpComputePricing();
1697
+ const regionPrices = pricing[region];
1698
+ if (!regionPrices) {
1699
+ logger.debug("GCP compute: region not found in bundled data", {
1700
+ region,
1701
+ machineType
1702
+ });
1703
+ return null;
1704
+ }
1705
+ const price = regionPrices[machineType];
1706
+ if (price === void 0) {
1707
+ logger.debug("GCP compute: machine type not found in bundled data", {
1708
+ region,
1709
+ machineType
1710
+ });
1711
+ return null;
1712
+ }
1713
+ return normalizeGcpCompute(machineType, price, region);
1714
+ } catch (err) {
1715
+ logger.error("GCP compute pricing load error", {
1716
+ machineType,
1717
+ region,
1718
+ err: err instanceof Error ? err.message : String(err)
1719
+ });
1720
+ return null;
1721
+ }
1722
+ }
1723
+ async getDatabasePrice(tier, region) {
1724
+ try {
1725
+ const pricing = getGcpSqlPricing();
1726
+ const regionPrices = pricing[region];
1727
+ if (!regionPrices) {
1728
+ logger.debug("GCP SQL: region not found in bundled data", {
1729
+ region,
1730
+ tier
1731
+ });
1732
+ return null;
1733
+ }
1734
+ const price = regionPrices[tier];
1735
+ if (price === void 0 || tier === "storage_per_gb" || tier === "ha_multiplier") {
1736
+ logger.debug("GCP SQL: tier not found in bundled data", {
1737
+ region,
1738
+ tier
1739
+ });
1740
+ return null;
1741
+ }
1742
+ return normalizeGcpDatabase(tier, price, region);
1743
+ } catch (err) {
1744
+ logger.error("GCP SQL pricing load error", {
1745
+ tier,
1746
+ region,
1747
+ err: err instanceof Error ? err.message : String(err)
1748
+ });
1749
+ return null;
1750
+ }
1751
+ }
1752
+ async getStoragePrice(storageClass, region) {
1753
+ try {
1754
+ const pricing = getGcpStoragePricing();
1755
+ const regionPrices = pricing[region];
1756
+ if (!regionPrices) {
1757
+ logger.debug("GCP Storage: region not found in bundled data", {
1758
+ region,
1759
+ storageClass
1760
+ });
1761
+ return null;
1762
+ }
1763
+ const classKey = storageClass.toUpperCase();
1764
+ const price = regionPrices[classKey];
1765
+ if (price === void 0) {
1766
+ logger.debug("GCP Storage: storage class not found in bundled data", {
1767
+ region,
1768
+ storageClass
1769
+ });
1770
+ return null;
1771
+ }
1772
+ return normalizeGcpStorage(storageClass.toUpperCase(), price, region);
1773
+ } catch (err) {
1774
+ logger.error("GCP Storage pricing load error", {
1775
+ storageClass,
1776
+ region,
1777
+ err: err instanceof Error ? err.message : String(err)
1778
+ });
1779
+ return null;
1780
+ }
1781
+ }
1782
+ async getDiskPrice(diskType, region) {
1783
+ try {
1784
+ const pricing = getGcpDiskPricing();
1785
+ const regionPrices = pricing[region];
1786
+ if (!regionPrices) {
1787
+ logger.debug("GCP Disk: region not found in bundled data", {
1788
+ region,
1789
+ diskType
1790
+ });
1791
+ return null;
1792
+ }
1793
+ const diskKey = diskType;
1794
+ const price = regionPrices[diskKey];
1795
+ if (price === void 0) {
1796
+ logger.debug("GCP Disk: disk type not found in bundled data", {
1797
+ region,
1798
+ diskType
1799
+ });
1800
+ return null;
1801
+ }
1802
+ return normalizeGcpDisk(diskType, price, region);
1803
+ } catch (err) {
1804
+ logger.error("GCP Disk pricing load error", {
1805
+ diskType,
1806
+ region,
1807
+ err: err instanceof Error ? err.message : String(err)
1808
+ });
1809
+ return null;
1810
+ }
1811
+ }
1812
+ async getLoadBalancerPrice(region) {
1813
+ return {
1814
+ provider: "gcp",
1815
+ service: "cloud-load-balancing",
1816
+ resource_type: "forwarding-rule",
1817
+ region,
1818
+ unit: "h",
1819
+ price_per_unit: LB_FORWARDING_RULE_HOURLY,
1820
+ currency: "USD",
1821
+ description: "GCP Cloud Load Balancing forwarding rule (per hour + data processed)",
1822
+ attributes: {
1823
+ per_gb_price: String(LB_PER_GB)
1824
+ },
1825
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1826
+ };
1827
+ }
1828
+ async getNatGatewayPrice(region) {
1829
+ return {
1830
+ provider: "gcp",
1831
+ service: "cloud-nat",
1832
+ resource_type: "nat-gateway",
1833
+ region,
1834
+ unit: "h",
1835
+ price_per_unit: NAT_HOURLY3,
1836
+ currency: "USD",
1837
+ description: "GCP Cloud NAT (per gateway/hour + data processed)",
1838
+ attributes: {
1839
+ per_gb_price: String(NAT_PER_GB3)
1840
+ },
1841
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1842
+ };
1843
+ }
1844
+ async getKubernetesPrice(region, mode = "standard") {
1845
+ const hourlyPrice = mode === "autopilot" ? GKE_AUTOPILOT_VCPU_HOURLY : GKE_STANDARD_HOURLY;
1846
+ return {
1847
+ provider: "gcp",
1848
+ service: "gke",
1849
+ resource_type: "cluster",
1850
+ region,
1851
+ unit: "h",
1852
+ price_per_unit: hourlyPrice,
1853
+ currency: "USD",
1854
+ description: mode === "autopilot" ? "GCP GKE Autopilot cluster (estimated per-vCPU/hr; actual billing is per-pod resources)" : "GCP GKE Standard cluster (per cluster/hour)",
1855
+ attributes: {
1856
+ mode,
1857
+ ...mode === "autopilot" && {
1858
+ pricing_model: "per_vcpu_hour",
1859
+ pricing_note: "Autopilot charges per pod vCPU and memory. This price is a per-vCPU/hr approximation for cluster-level cost estimation."
1860
+ }
1861
+ },
1862
+ effective_date: (/* @__PURE__ */ new Date()).toISOString()
1863
+ };
1864
+ }
1865
+ };
1866
+
1867
+ // src/pricing/pricing-engine.ts
1868
+ var AwsProvider = class {
1869
+ loader;
1870
+ constructor(cache) {
1871
+ this.loader = new AwsBulkLoader(cache);
1872
+ }
1873
+ getComputePrice(instanceType, region, os) {
1874
+ return this.loader.getComputePrice(instanceType, region, os);
1875
+ }
1876
+ getDatabasePrice(instanceType, region, engine) {
1877
+ return this.loader.getDatabasePrice(instanceType, region, engine);
1878
+ }
1879
+ getStoragePrice(storageType, region, _sizeGb) {
1880
+ return this.loader.getStoragePrice(storageType, region);
1881
+ }
1882
+ getLoadBalancerPrice(_type, region) {
1883
+ return this.loader.getLoadBalancerPrice(region);
1884
+ }
1885
+ getNatGatewayPrice(region) {
1886
+ return this.loader.getNatGatewayPrice(region);
1887
+ }
1888
+ getKubernetesPrice(region) {
1889
+ return this.loader.getKubernetesPrice(region);
1890
+ }
1891
+ };
1892
+ var AzureProvider = class {
1893
+ client;
1894
+ constructor(cache) {
1895
+ this.client = new AzureRetailClient(cache);
1896
+ }
1897
+ getComputePrice(instanceType, region, os) {
1898
+ return this.client.getComputePrice(instanceType, region, os);
1899
+ }
1900
+ getDatabasePrice(instanceType, region, engine) {
1901
+ return this.client.getDatabasePrice(instanceType, region, engine);
1902
+ }
1903
+ getStoragePrice(storageType, region, _sizeGb) {
1904
+ return this.client.getStoragePrice(storageType, region);
1905
+ }
1906
+ getLoadBalancerPrice(_type, region) {
1907
+ return this.client.getLoadBalancerPrice(region);
1908
+ }
1909
+ getNatGatewayPrice(region) {
1910
+ return this.client.getNatGatewayPrice(region);
1911
+ }
1912
+ getKubernetesPrice(region) {
1913
+ return this.client.getKubernetesPrice(region);
1914
+ }
1915
+ };
1916
+ var GcpProvider = class {
1917
+ loader;
1918
+ constructor() {
1919
+ this.loader = new GcpBundledLoader();
1920
+ }
1921
+ getComputePrice(instanceType, region, _os) {
1922
+ return this.loader.getComputePrice(instanceType, region);
1923
+ }
1924
+ getDatabasePrice(instanceType, region, _engine) {
1925
+ return this.loader.getDatabasePrice(instanceType, region);
1926
+ }
1927
+ getStoragePrice(storageType, region, _sizeGb) {
1928
+ if (storageType.startsWith("pd-")) {
1929
+ return this.loader.getDiskPrice(storageType, region);
1930
+ }
1931
+ return this.loader.getStoragePrice(storageType, region);
1932
+ }
1933
+ getLoadBalancerPrice(_type, region) {
1934
+ return this.loader.getLoadBalancerPrice(region);
1935
+ }
1936
+ getNatGatewayPrice(region) {
1937
+ return this.loader.getNatGatewayPrice(region);
1938
+ }
1939
+ getKubernetesPrice(region) {
1940
+ return this.loader.getKubernetesPrice(region);
1941
+ }
1942
+ };
1943
+ var PricingEngine = class {
1944
+ providers = /* @__PURE__ */ new Map();
1945
+ constructor(cache, _config) {
1946
+ this.providers.set("aws", new AwsProvider(cache));
1947
+ this.providers.set("azure", new AzureProvider(cache));
1948
+ this.providers.set("gcp", new GcpProvider());
1949
+ }
1950
+ /**
1951
+ * Return the PricingProvider for a specific cloud provider.
1952
+ * Throws if the provider is not registered (should never happen for the
1953
+ * three known providers).
1954
+ */
1955
+ getProvider(provider) {
1956
+ const p = this.providers.get(provider);
1957
+ if (!p) {
1958
+ throw new Error(`Unknown cloud provider: ${provider}`);
1959
+ }
1960
+ return p;
1961
+ }
1962
+ /**
1963
+ * Generic price lookup that maps service/resourceType strings to the
1964
+ * appropriate method on the underlying PricingProvider.
1965
+ *
1966
+ * Supported service values (case-insensitive):
1967
+ * compute, ec2, vm, instance → getComputePrice
1968
+ * database, rds, sql, db → getDatabasePrice
1969
+ * storage, ebs, disk, gcs → getStoragePrice
1970
+ * lb, load-balancer, alb, nlb → getLoadBalancerPrice
1971
+ * nat, nat-gateway → getNatGatewayPrice
1972
+ * k8s, kubernetes, eks, aks, gke → getKubernetesPrice
1973
+ */
1974
+ async getPrice(provider, service, resourceType, region, attributes = {}) {
1975
+ const p = this.getProvider(provider);
1976
+ const svc = service.toLowerCase();
1977
+ logger.debug("PricingEngine.getPrice", {
1978
+ provider,
1979
+ service,
1980
+ resourceType,
1981
+ region
1982
+ });
1983
+ if (svc === "compute" || svc === "ec2" || svc === "vm" || svc === "instance" || svc === "virtual-machines") {
1984
+ return p.getComputePrice(resourceType, region, attributes.os);
1985
+ }
1986
+ if (svc === "database" || svc === "rds" || svc === "sql" || svc === "db" || svc === "cloud-sql" || svc === "azure-database") {
1987
+ return p.getDatabasePrice(resourceType, region, attributes.engine);
1988
+ }
1989
+ if (svc === "storage" || svc === "ebs" || svc === "disk" || svc === "gcs" || svc === "managed-disks" || svc === "persistent-disk" || svc === "cloud-storage") {
1990
+ const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
1991
+ return p.getStoragePrice(resourceType, region, sizeGb);
1992
+ }
1993
+ if (svc === "lb" || svc === "load-balancer" || svc === "alb" || svc === "nlb" || svc === "elb" || svc === "cloud-load-balancing") {
1994
+ return p.getLoadBalancerPrice(resourceType, region);
1995
+ }
1996
+ if (svc === "nat" || svc === "nat-gateway" || svc === "cloud-nat") {
1997
+ return p.getNatGatewayPrice(region);
1998
+ }
1999
+ if (svc === "k8s" || svc === "kubernetes" || svc === "eks" || svc === "aks" || svc === "gke") {
2000
+ return p.getKubernetesPrice(region);
2001
+ }
2002
+ logger.warn("PricingEngine: unrecognised service", { service, provider });
2003
+ return null;
2004
+ }
2005
+ };
2006
+
2007
+ // src/tools/analyze-terraform.ts
2008
+ import { z } from "zod";
2009
+
2010
+ // src/parsers/hcl-parser.ts
2011
+ import { parse } from "@cdktf/hcl2json";
2012
+ async function parseHclToJson(hclContent, filename = "main.tf") {
2013
+ if (!hclContent.trim()) {
2014
+ logger.debug("Received empty HCL content, returning empty object", {
2015
+ filename
2016
+ });
2017
+ return {};
2018
+ }
2019
+ try {
2020
+ const result = await parse(filename, hclContent);
2021
+ logger.debug("Parsed HCL successfully", { filename });
2022
+ return result;
2023
+ } catch (err) {
2024
+ const message = err instanceof Error ? err.message : String(err);
2025
+ logger.error("Failed to parse HCL", { filename, error: message });
2026
+ throw new Error(
2027
+ `HCL parse error in "${filename}": ${message}`
2028
+ );
2029
+ }
2030
+ }
2031
+
2032
+ // src/parsers/variable-resolver.ts
2033
+ function resolveVariables(hclJson, tfvarsContent) {
2034
+ const defaults = extractVariableDefaults(hclJson);
2035
+ const overrides = tfvarsContent ? parseTfvars(tfvarsContent) : {};
2036
+ const resolved = { ...defaults, ...overrides };
2037
+ logger.debug("Resolved variables", {
2038
+ defaultCount: Object.keys(defaults).length,
2039
+ overrideCount: Object.keys(overrides).length
2040
+ });
2041
+ return resolved;
2042
+ }
2043
+ function extractVariableDefaults(hclJson) {
2044
+ const defaults = {};
2045
+ const variableBlock = hclJson["variable"];
2046
+ if (!variableBlock || typeof variableBlock !== "object") {
2047
+ return defaults;
2048
+ }
2049
+ for (const [varName, declarations] of Object.entries(
2050
+ variableBlock
2051
+ )) {
2052
+ if (!Array.isArray(declarations) || declarations.length === 0) continue;
2053
+ const declaration = declarations[0];
2054
+ if ("default" in declaration) {
2055
+ defaults[varName] = declaration["default"];
2056
+ }
2057
+ }
2058
+ return defaults;
2059
+ }
2060
+ function parseTfvars(content) {
2061
+ const result = {};
2062
+ for (const rawLine of content.split("\n")) {
2063
+ const line = rawLine.trim();
2064
+ if (!line || line.startsWith("#")) continue;
2065
+ const eqIndex = line.indexOf("=");
2066
+ if (eqIndex === -1) continue;
2067
+ const key = line.slice(0, eqIndex).trim();
2068
+ const rawValue = line.slice(eqIndex + 1).trim();
2069
+ if (!key) continue;
2070
+ result[key] = parseTfvarsValue(rawValue);
2071
+ }
2072
+ return result;
2073
+ }
2074
+ function parseTfvarsValue(raw) {
2075
+ if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
2076
+ return raw.slice(1, -1);
2077
+ }
2078
+ if (raw === "true") return true;
2079
+ if (raw === "false") return false;
2080
+ if (raw === "null") return null;
2081
+ if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
2082
+ if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
2083
+ return raw;
2084
+ }
2085
+ function substituteVariables(value, variables) {
2086
+ if (typeof value === "string") {
2087
+ return substituteInString(value, variables);
2088
+ }
2089
+ if (Array.isArray(value)) {
2090
+ return value.map((item) => substituteVariables(item, variables));
2091
+ }
2092
+ if (value !== null && typeof value === "object") {
2093
+ const result = {};
2094
+ for (const [k, v] of Object.entries(value)) {
2095
+ result[k] = substituteVariables(v, variables);
2096
+ }
2097
+ return result;
2098
+ }
2099
+ return value;
2100
+ }
2101
+ var VAR_ONLY_RE = /^\$\{var\.([^}]+)\}$/;
2102
+ var VAR_INLINE_RE = /\$\{var\.([^}]+)\}/g;
2103
+ function substituteInString(str2, variables) {
2104
+ const wholeMatch = VAR_ONLY_RE.exec(str2);
2105
+ if (wholeMatch) {
2106
+ const varName = wholeMatch[1];
2107
+ return varName in variables ? variables[varName] : str2;
2108
+ }
2109
+ return str2.replace(VAR_INLINE_RE, (_match, varName) => {
2110
+ if (varName in variables) {
2111
+ const v = variables[varName];
2112
+ return v === null ? "" : String(v);
2113
+ }
2114
+ return _match;
2115
+ });
2116
+ }
2117
+
2118
+ // src/parsers/provider-detector.ts
2119
+ var PROVIDER_PREFIXES = [
2120
+ ["aws_", "aws"],
2121
+ ["azurerm_", "azure"],
2122
+ ["google_", "gcp"]
2123
+ ];
2124
+ function detectProvider(resourceType) {
2125
+ for (const [prefix, provider] of PROVIDER_PREFIXES) {
2126
+ if (resourceType.startsWith(prefix)) {
2127
+ return provider;
2128
+ }
2129
+ }
2130
+ throw new Error(
2131
+ `Cannot determine cloud provider for resource type "${resourceType}". Supported prefixes: aws_, azurerm_, google_`
2132
+ );
2133
+ }
2134
+
2135
+ // src/parsers/resource-extractor.ts
2136
+ function detectRegionFromProviders(hclJson, provider, variables) {
2137
+ const defaults = {
2138
+ aws: "us-east-1",
2139
+ azure: "eastus",
2140
+ gcp: "us-central1"
2141
+ };
2142
+ const providerBlock = hclJson["provider"];
2143
+ if (!providerBlock || typeof providerBlock !== "object") {
2144
+ return defaults[provider];
2145
+ }
2146
+ const providerKeyMap = {
2147
+ aws: "aws",
2148
+ azure: "azurerm",
2149
+ gcp: "google"
2150
+ };
2151
+ const key = providerKeyMap[provider];
2152
+ const declarations = providerBlock[key];
2153
+ if (!Array.isArray(declarations) || declarations.length === 0) {
2154
+ return defaults[provider];
2155
+ }
2156
+ const declaration = declarations[0];
2157
+ const regionAttr = provider === "azure" ? "location" : "region";
2158
+ const rawRegion = declaration[regionAttr];
2159
+ if (!rawRegion) return defaults[provider];
2160
+ const resolved = substituteVariables(rawRegion, variables);
2161
+ if (typeof resolved === "string" && resolved && !resolved.startsWith("${")) {
2162
+ return resolved;
2163
+ }
2164
+ return defaults[provider];
2165
+ }
2166
+ function firstBlock(block, key) {
2167
+ const nested = block[key];
2168
+ if (Array.isArray(nested) && nested.length > 0) {
2169
+ return nested[0];
2170
+ }
2171
+ return {};
2172
+ }
2173
+ function str(v) {
2174
+ return typeof v === "string" && v ? v : void 0;
2175
+ }
2176
+ function num(v) {
2177
+ if (typeof v === "number") return v;
2178
+ if (typeof v === "string") {
2179
+ const n = Number(v);
2180
+ return isNaN(n) ? void 0 : n;
2181
+ }
2182
+ return void 0;
2183
+ }
2184
+ function bool(v) {
2185
+ if (typeof v === "boolean") return v;
2186
+ if (v === "true") return true;
2187
+ if (v === "false") return false;
2188
+ return void 0;
2189
+ }
2190
+ var extractors = {
2191
+ aws_instance(block) {
2192
+ const root = firstBlock(block, "root_block_device");
2193
+ const attrs = {};
2194
+ if (str(block["instance_type"])) attrs.instance_type = str(block["instance_type"]);
2195
+ if (str(block["ami"])) attrs.ami = str(block["ami"]);
2196
+ if (str(block["availability_zone"])) attrs.availability_zone = str(block["availability_zone"]);
2197
+ if (root["volume_type"]) attrs.storage_type = str(root["volume_type"]);
2198
+ if (root["volume_size"] !== void 0) attrs.storage_size_gb = num(root["volume_size"]);
2199
+ if (root["iops"] !== void 0) attrs.iops = num(root["iops"]);
2200
+ if (root["throughput"] !== void 0) attrs.throughput_mbps = num(root["throughput"]);
2201
+ return attrs;
2202
+ },
2203
+ aws_db_instance(block) {
2204
+ const attrs = {};
2205
+ if (str(block["instance_class"])) attrs.instance_type = str(block["instance_class"]);
2206
+ if (str(block["engine"])) attrs.engine = str(block["engine"]);
2207
+ if (str(block["engine_version"])) attrs.engine_version = str(block["engine_version"]);
2208
+ if (str(block["storage_type"])) attrs.storage_type = str(block["storage_type"]);
2209
+ if (block["allocated_storage"] !== void 0) attrs.storage_size_gb = num(block["allocated_storage"]);
2210
+ if (block["iops"] !== void 0) attrs.iops = num(block["iops"]);
2211
+ if (block["multi_az"] !== void 0) attrs.multi_az = bool(block["multi_az"]);
2212
+ if (block["replicas"] !== void 0) attrs.replicas = num(block["replicas"]);
2213
+ return attrs;
2214
+ },
2215
+ aws_rds_cluster(block) {
2216
+ const attrs = {};
2217
+ if (str(block["engine"])) attrs.engine = str(block["engine"]);
2218
+ if (str(block["engine_version"])) attrs.engine_version = str(block["engine_version"]);
2219
+ if (str(block["db_cluster_instance_class"])) attrs.instance_type = str(block["db_cluster_instance_class"]);
2220
+ if (block["master_username"]) attrs.master_username = str(block["master_username"]);
2221
+ return attrs;
2222
+ },
2223
+ aws_s3_bucket(block) {
2224
+ const attrs = {};
2225
+ const lifecycle = firstBlock(block, "lifecycle_rule");
2226
+ const transition = firstBlock(lifecycle, "transition");
2227
+ if (str(transition["storage_class"])) attrs.storage_type = str(transition["storage_class"]);
2228
+ if (str(block["bucket"])) attrs.bucket = str(block["bucket"]);
2229
+ return attrs;
2230
+ },
2231
+ aws_lb(block) {
2232
+ const attrs = {};
2233
+ if (str(block["load_balancer_type"])) attrs.load_balancer_type = str(block["load_balancer_type"]);
2234
+ if (block["internal"] !== void 0) attrs.internal = bool(block["internal"]);
2235
+ return attrs;
2236
+ },
2237
+ aws_alb(block) {
2238
+ return extractors["aws_lb"](block);
2239
+ },
2240
+ aws_nat_gateway(block) {
2241
+ const attrs = {};
2242
+ if (str(block["allocation_id"])) attrs.allocation_id = str(block["allocation_id"]);
2243
+ return attrs;
2244
+ },
2245
+ aws_eks_cluster(block) {
2246
+ const attrs = {};
2247
+ if (str(block["name"])) attrs.cluster_name = str(block["name"]);
2248
+ return attrs;
2249
+ },
2250
+ aws_eks_node_group(block) {
2251
+ const scaling = firstBlock(block, "scaling_config");
2252
+ const attrs = {};
2253
+ const instanceTypes = block["instance_types"];
2254
+ if (Array.isArray(instanceTypes) && instanceTypes.length > 0) {
2255
+ attrs.instance_type = str(instanceTypes[0]);
2256
+ }
2257
+ if (scaling["desired_size"] !== void 0) attrs.node_count = num(scaling["desired_size"]);
2258
+ if (scaling["min_size"] !== void 0) attrs.min_node_count = num(scaling["min_size"]);
2259
+ if (scaling["max_size"] !== void 0) attrs.max_node_count = num(scaling["max_size"]);
2260
+ return attrs;
2261
+ },
2262
+ aws_elasticache_cluster(block) {
2263
+ const attrs = {};
2264
+ if (str(block["node_type"])) attrs.instance_type = str(block["node_type"]);
2265
+ if (str(block["engine"])) attrs.engine = str(block["engine"]);
2266
+ if (block["num_cache_nodes"] !== void 0) attrs.node_count = num(block["num_cache_nodes"]);
2267
+ return attrs;
2268
+ },
2269
+ aws_elasticache_replication_group(block) {
2270
+ const attrs = {};
2271
+ if (str(block["node_type"])) attrs.instance_type = str(block["node_type"]);
2272
+ if (block["num_cache_clusters"] !== void 0) attrs.node_count = num(block["num_cache_clusters"]);
2273
+ return attrs;
2274
+ },
2275
+ aws_ebs_volume(block) {
2276
+ const attrs = {};
2277
+ if (str(block["type"])) attrs.storage_type = str(block["type"]);
2278
+ if (block["size"] !== void 0) attrs.storage_size_gb = num(block["size"]);
2279
+ if (block["iops"] !== void 0) attrs.iops = num(block["iops"]);
2280
+ if (block["throughput"] !== void 0) attrs.throughput_mbps = num(block["throughput"]);
2281
+ return attrs;
2282
+ },
2283
+ aws_lambda_function(block) {
2284
+ const attrs = {};
2285
+ if (block["memory_size"] !== void 0) attrs.memory_size = num(block["memory_size"]);
2286
+ if (block["timeout"] !== void 0) attrs.timeout = num(block["timeout"]);
2287
+ if (str(block["architecture"])) {
2288
+ attrs.architecture = str(block["architecture"]);
2289
+ } else {
2290
+ const arch = block["architectures"];
2291
+ if (Array.isArray(arch) && arch.length > 0) {
2292
+ attrs.architecture = str(arch[0]);
2293
+ }
2294
+ }
2295
+ if (str(block["runtime"])) attrs.runtime = str(block["runtime"]);
2296
+ return attrs;
2297
+ },
2298
+ aws_dynamodb_table(block) {
2299
+ const attrs = {};
2300
+ if (str(block["billing_mode"])) attrs.billing_mode = str(block["billing_mode"]);
2301
+ if (block["read_capacity"] !== void 0) attrs.read_capacity = num(block["read_capacity"]);
2302
+ if (block["write_capacity"] !== void 0) attrs.write_capacity = num(block["write_capacity"]);
2303
+ return attrs;
2304
+ },
2305
+ aws_efs_file_system(block) {
2306
+ const attrs = {};
2307
+ if (str(block["performance_mode"])) attrs.performance_mode = str(block["performance_mode"]);
2308
+ if (str(block["throughput_mode"])) attrs.throughput_mode = str(block["throughput_mode"]);
2309
+ return attrs;
2310
+ },
2311
+ aws_cloudfront_distribution(block) {
2312
+ const attrs = {};
2313
+ if (str(block["price_class"])) attrs.price_class = str(block["price_class"]);
2314
+ return attrs;
2315
+ },
2316
+ aws_sqs_queue(block) {
2317
+ const attrs = {};
2318
+ if (block["fifo_queue"] !== void 0) attrs.fifo_queue = bool(block["fifo_queue"]);
2319
+ return attrs;
2320
+ },
2321
+ // ---------------------------------------------------------------------------
2322
+ // Azure
2323
+ // ---------------------------------------------------------------------------
2324
+ azurerm_virtual_machine(block) {
2325
+ const attrs = {};
2326
+ if (str(block["vm_size"])) attrs.vm_size = str(block["vm_size"]);
2327
+ if (str(block["location"])) attrs.location = str(block["location"]);
2328
+ return attrs;
2329
+ },
2330
+ azurerm_linux_virtual_machine(block) {
2331
+ const attrs = {};
2332
+ if (str(block["size"])) attrs.vm_size = str(block["size"]);
2333
+ if (str(block["location"])) attrs.location = str(block["location"]);
2334
+ return attrs;
2335
+ },
2336
+ azurerm_windows_virtual_machine(block) {
2337
+ const attrs = {};
2338
+ if (str(block["size"])) attrs.vm_size = str(block["size"]);
2339
+ if (str(block["location"])) attrs.location = str(block["location"]);
2340
+ attrs.os = "Windows";
2341
+ return attrs;
2342
+ },
2343
+ azurerm_managed_disk(block) {
2344
+ const attrs = {};
2345
+ if (str(block["storage_account_type"])) attrs.storage_type = str(block["storage_account_type"]);
2346
+ if (block["disk_size_gb"] !== void 0) attrs.storage_size_gb = num(block["disk_size_gb"]);
2347
+ if (block["disk_iops_read_write"] !== void 0) attrs.iops = num(block["disk_iops_read_write"]);
2348
+ if (str(block["location"])) attrs.location = str(block["location"]);
2349
+ return attrs;
2350
+ },
2351
+ azurerm_sql_server(block) {
2352
+ const attrs = {};
2353
+ if (str(block["version"])) attrs.engine_version = str(block["version"]);
2354
+ if (str(block["location"])) attrs.location = str(block["location"]);
2355
+ return attrs;
2356
+ },
2357
+ azurerm_mssql_database(block) {
2358
+ const attrs = {};
2359
+ if (str(block["sku_name"])) attrs.sku = str(block["sku_name"]);
2360
+ if (block["max_size_gb"] !== void 0) attrs.storage_size_gb = num(block["max_size_gb"]);
2361
+ return attrs;
2362
+ },
2363
+ azurerm_kubernetes_cluster(block) {
2364
+ const defaultPool = firstBlock(block, "default_node_pool");
2365
+ const attrs = {};
2366
+ if (str(defaultPool["vm_size"])) attrs.vm_size = str(defaultPool["vm_size"]);
2367
+ if (defaultPool["node_count"] !== void 0) attrs.node_count = num(defaultPool["node_count"]);
2368
+ if (defaultPool["min_count"] !== void 0) attrs.min_node_count = num(defaultPool["min_count"]);
2369
+ if (defaultPool["max_count"] !== void 0) attrs.max_node_count = num(defaultPool["max_count"]);
2370
+ if (str(block["location"])) attrs.location = str(block["location"]);
2371
+ return attrs;
2372
+ },
2373
+ azurerm_cosmosdb_account(block) {
2374
+ const attrs = {};
2375
+ if (str(block["offer_type"])) attrs.offer_type = str(block["offer_type"]);
2376
+ if (str(block["kind"])) attrs.kind = str(block["kind"]);
2377
+ const capabilities = block["capabilities"];
2378
+ if (Array.isArray(capabilities)) {
2379
+ const capNames = capabilities.map((c) => typeof c === "object" && c !== null ? str(c["name"]) : void 0).filter((n) => n !== void 0);
2380
+ if (capNames.length > 0) attrs.capabilities = capNames;
2381
+ }
2382
+ return attrs;
2383
+ },
2384
+ azurerm_app_service_plan(block) {
2385
+ const attrs = {};
2386
+ const skuBlock = firstBlock(block, "sku");
2387
+ if (str(skuBlock["tier"])) attrs.sku_tier = str(skuBlock["tier"]);
2388
+ if (str(skuBlock["size"])) attrs.sku_size = str(skuBlock["size"]);
2389
+ if (str(block["sku_name"])) attrs.sku = str(block["sku_name"]);
2390
+ return attrs;
2391
+ },
2392
+ azurerm_function_app(block) {
2393
+ const attrs = {};
2394
+ if (str(block["app_service_plan_id"])) attrs.app_service_plan_id = str(block["app_service_plan_id"]);
2395
+ return attrs;
2396
+ },
2397
+ azurerm_redis_cache(block) {
2398
+ const attrs = {};
2399
+ if (block["capacity"] !== void 0) attrs.capacity = num(block["capacity"]);
2400
+ if (str(block["family"])) attrs.family = str(block["family"]);
2401
+ if (str(block["sku_name"])) attrs.sku = str(block["sku_name"]);
2402
+ return attrs;
2403
+ },
2404
+ azurerm_cosmosdb_sql_database(block) {
2405
+ const attrs = {};
2406
+ if (block["throughput"] !== void 0) attrs.throughput = num(block["throughput"]);
2407
+ return attrs;
2408
+ },
2409
+ azurerm_storage_account(block) {
2410
+ const attrs = {};
2411
+ if (str(block["account_tier"])) attrs.account_tier = str(block["account_tier"]);
2412
+ if (str(block["account_replication_type"])) attrs.account_replication_type = str(block["account_replication_type"]);
2413
+ return attrs;
2414
+ },
2415
+ // ---------------------------------------------------------------------------
2416
+ // GCP
2417
+ // ---------------------------------------------------------------------------
2418
+ google_compute_instance(block) {
2419
+ const attrs = {};
2420
+ if (str(block["machine_type"])) attrs.machine_type = str(block["machine_type"]);
2421
+ if (str(block["zone"])) attrs.zone = str(block["zone"]);
2422
+ const boot = firstBlock(block, "boot_disk");
2423
+ const initParams = firstBlock(boot, "initialize_params");
2424
+ if (str(initParams["type"])) attrs.storage_type = str(initParams["type"]);
2425
+ if (initParams["size"] !== void 0) attrs.storage_size_gb = num(initParams["size"]);
2426
+ return attrs;
2427
+ },
2428
+ google_compute_disk(block) {
2429
+ const attrs = {};
2430
+ if (str(block["type"])) attrs.storage_type = str(block["type"]);
2431
+ if (block["size"] !== void 0) attrs.storage_size_gb = num(block["size"]);
2432
+ if (str(block["zone"])) attrs.zone = str(block["zone"]);
2433
+ return attrs;
2434
+ },
2435
+ google_sql_database_instance(block) {
2436
+ const settings = firstBlock(block, "settings");
2437
+ const attrs = {};
2438
+ if (str(settings["tier"])) attrs.tier = str(settings["tier"]);
2439
+ if (str(block["database_version"])) attrs.engine = str(block["database_version"]);
2440
+ if (str(block["region"])) attrs.region_attr = str(block["region"]);
2441
+ const dataCacheConfig = firstBlock(settings, "data_cache_config");
2442
+ if (dataCacheConfig["data_cache_enabled"] !== void 0) {
2443
+ attrs.data_cache_enabled = bool(dataCacheConfig["data_cache_enabled"]);
2444
+ }
2445
+ return attrs;
2446
+ },
2447
+ google_container_cluster(block) {
2448
+ const nodeConfig = firstBlock(block, "node_config");
2449
+ const attrs = {};
2450
+ if (str(nodeConfig["machine_type"])) attrs.machine_type = str(nodeConfig["machine_type"]);
2451
+ if (block["initial_node_count"] !== void 0) attrs.node_count = num(block["initial_node_count"]);
2452
+ if (str(block["location"])) attrs.zone = str(block["location"]);
2453
+ return attrs;
2454
+ },
2455
+ google_container_node_pool(block) {
2456
+ const nodeConfig = firstBlock(block, "node_config");
2457
+ const autoscaling = firstBlock(block, "autoscaling");
2458
+ const attrs = {};
2459
+ if (str(nodeConfig["machine_type"])) attrs.machine_type = str(nodeConfig["machine_type"]);
2460
+ if (block["node_count"] !== void 0) attrs.node_count = num(block["node_count"]);
2461
+ if (autoscaling["min_node_count"] !== void 0) attrs.min_node_count = num(autoscaling["min_node_count"]);
2462
+ if (autoscaling["max_node_count"] !== void 0) attrs.max_node_count = num(autoscaling["max_node_count"]);
2463
+ return attrs;
2464
+ },
2465
+ google_cloud_run_service(block) {
2466
+ const attrs = {};
2467
+ const template = firstBlock(block, "template");
2468
+ const spec = firstBlock(template, "spec");
2469
+ const container = firstBlock(spec, "containers");
2470
+ const resources = firstBlock(container, "resources");
2471
+ const limits = firstBlock(resources, "limits");
2472
+ if (str(limits["cpu"])) attrs.cpu = str(limits["cpu"]);
2473
+ if (str(limits["memory"])) attrs.memory = str(limits["memory"]);
2474
+ return attrs;
2475
+ },
2476
+ google_cloudfunctions_function(block) {
2477
+ const attrs = {};
2478
+ if (str(block["runtime"])) attrs.runtime = str(block["runtime"]);
2479
+ if (block["available_memory_mb"] !== void 0) attrs.memory_size = num(block["available_memory_mb"]);
2480
+ return attrs;
2481
+ },
2482
+ google_bigquery_dataset(block) {
2483
+ const attrs = {};
2484
+ if (str(block["location"])) attrs.location = str(block["location"]);
2485
+ return attrs;
2486
+ },
2487
+ google_redis_instance(block) {
2488
+ const attrs = {};
2489
+ if (str(block["tier"])) attrs.tier = str(block["tier"]);
2490
+ if (block["memory_size_gb"] !== void 0) attrs.memory_size = num(block["memory_size_gb"]);
2491
+ return attrs;
2492
+ },
2493
+ google_artifact_registry_repository(block) {
2494
+ const attrs = {};
2495
+ if (str(block["format"])) attrs.format = str(block["format"]);
2496
+ return attrs;
2497
+ }
2498
+ };
2499
+ function regionFromResourceBlock(resourceType, block, provider) {
2500
+ switch (provider) {
2501
+ case "azure": {
2502
+ const loc = str(block["location"]);
2503
+ return loc && !loc.startsWith("${") ? loc : void 0;
2504
+ }
2505
+ case "gcp": {
2506
+ const zone = str(block["zone"]);
2507
+ if (zone && !zone.startsWith("${")) {
2508
+ const parts = zone.split("-");
2509
+ return parts.length > 2 ? parts.slice(0, -1).join("-") : zone;
2510
+ }
2511
+ return str(block["region"]);
2512
+ }
2513
+ case "aws": {
2514
+ const az = str(block["availability_zone"]);
2515
+ if (az && !az.startsWith("${")) {
2516
+ const parts = az.split("-");
2517
+ return parts.length > 2 ? parts.slice(0, -1).join("-") : az;
2518
+ }
2519
+ return void 0;
2520
+ }
2521
+ }
2522
+ }
2523
+ function extractTags(block) {
2524
+ const tags = block["tags"];
2525
+ if (!tags || typeof tags !== "object" || Array.isArray(tags)) return {};
2526
+ const result = {};
2527
+ for (const [k, v] of Object.entries(tags)) {
2528
+ if (typeof v === "string") result[k] = v;
2529
+ else if (v !== null && v !== void 0) result[k] = String(v);
2530
+ }
2531
+ return result;
2532
+ }
2533
+ function resolveCount(rawCount, resourceId, warnings) {
2534
+ const n = num(rawCount);
2535
+ if (n !== void 0 && Number.isInteger(n) && n >= 0) return n;
2536
+ warnings.push(
2537
+ `Resource "${resourceId}" has a count that could not be resolved to a number. Defaulting to 1.`
2538
+ );
2539
+ return 1;
2540
+ }
2541
+ function resolveForEachKeys(rawForEach, resourceId, warnings) {
2542
+ if (rawForEach && typeof rawForEach === "object" && !Array.isArray(rawForEach)) {
2543
+ const keys = Object.keys(rawForEach);
2544
+ if (keys.length > 0) return keys;
2545
+ }
2546
+ if (Array.isArray(rawForEach) && rawForEach.length > 0) {
2547
+ return rawForEach.map(
2548
+ (v, i) => typeof v === "string" ? v : typeof v === "number" ? String(v) : String(i)
2549
+ );
2550
+ }
2551
+ warnings.push(
2552
+ `Resource "${resourceId}" has a for_each that could not be resolved. Defaulting to 1 instance.`
2553
+ );
2554
+ return ["0"];
2555
+ }
2556
+ function extractResources(hclJson, variables, sourceFile, defaultRegions = {}, warnings = []) {
2557
+ const resources = [];
2558
+ const moduleBlock = hclJson["module"];
2559
+ if (moduleBlock && typeof moduleBlock === "object" && !Array.isArray(moduleBlock)) {
2560
+ for (const moduleName of Object.keys(moduleBlock)) {
2561
+ warnings.push(
2562
+ `Module "${moduleName}" detected but not expanded. Resources inside modules are not included in the estimate.`
2563
+ );
2564
+ logger.debug("Module detected, skipping expansion", { moduleName });
2565
+ }
2566
+ }
2567
+ const resourceBlock = hclJson["resource"];
2568
+ if (!resourceBlock || typeof resourceBlock !== "object") {
2569
+ return resources;
2570
+ }
2571
+ for (const [resourceType, instances] of Object.entries(
2572
+ resourceBlock
2573
+ )) {
2574
+ let provider;
2575
+ try {
2576
+ provider = detectProvider(resourceType);
2577
+ } catch {
2578
+ logger.warn("Skipping resource with unknown provider prefix", {
2579
+ resourceType
2580
+ });
2581
+ continue;
2582
+ }
2583
+ if (!instances || typeof instances !== "object" || Array.isArray(instances)) {
2584
+ continue;
2585
+ }
2586
+ for (const [resourceName, blockList] of Object.entries(
2587
+ instances
2588
+ )) {
2589
+ if (!Array.isArray(blockList) || blockList.length === 0) continue;
2590
+ const rawBlock = blockList[0];
2591
+ const block = substituteVariables(rawBlock, variables);
2592
+ const resourceRegion = regionFromResourceBlock(resourceType, block, provider);
2593
+ const region = resourceRegion ?? defaultRegions[provider] ?? (provider === "aws" ? "us-east-1" : provider === "azure" ? "eastus" : "us-central1");
2594
+ const extractor = extractors[resourceType];
2595
+ const attributes = extractor ? extractor(block) : {};
2596
+ const tags = extractTags(block);
2597
+ const baseId = `${resourceType}.${resourceName}`;
2598
+ const rawCount = block["count"];
2599
+ const rawForEach = block["for_each"];
2600
+ if (rawCount !== void 0) {
2601
+ const count = resolveCount(rawCount, baseId, warnings);
2602
+ for (let i = 0; i < count; i++) {
2603
+ resources.push({
2604
+ id: `${baseId}[${i}]`,
2605
+ type: resourceType,
2606
+ name: resourceName,
2607
+ provider,
2608
+ region,
2609
+ attributes,
2610
+ tags,
2611
+ source_file: sourceFile
2612
+ });
2613
+ }
2614
+ } else if (rawForEach !== void 0) {
2615
+ const keys = resolveForEachKeys(rawForEach, baseId, warnings);
2616
+ for (const key of keys) {
2617
+ resources.push({
2618
+ id: `${baseId}["${key}"]`,
2619
+ type: resourceType,
2620
+ name: resourceName,
2621
+ provider,
2622
+ region,
2623
+ attributes,
2624
+ tags,
2625
+ source_file: sourceFile
2626
+ });
2627
+ }
2628
+ } else {
2629
+ resources.push({
2630
+ id: baseId,
2631
+ type: resourceType,
2632
+ name: resourceName,
2633
+ provider,
2634
+ region,
2635
+ attributes,
2636
+ tags,
2637
+ source_file: sourceFile
2638
+ });
2639
+ }
2640
+ logger.debug("Extracted resource", {
2641
+ id: baseId,
2642
+ region,
2643
+ provider
2644
+ });
2645
+ }
2646
+ }
2647
+ return resources;
2648
+ }
2649
+
2650
+ // src/parsers/index.ts
2651
+ var PROVIDER_DEFAULTS = {
2652
+ aws: "us-east-1",
2653
+ azure: "eastus",
2654
+ gcp: "us-central1"
2655
+ };
2656
+ function inferDominantProvider(resources) {
2657
+ if (resources.length === 0) return "aws";
2658
+ const counts = {};
2659
+ for (const r of resources) {
2660
+ counts[r.provider] = (counts[r.provider] ?? 0) + 1;
2661
+ }
2662
+ let dominant = "aws";
2663
+ let max = 0;
2664
+ for (const [p, c] of Object.entries(counts)) {
2665
+ if (c > max) {
2666
+ max = c;
2667
+ dominant = p;
2668
+ }
2669
+ }
2670
+ return dominant;
2671
+ }
2672
+ async function parseTerraform(files, tfvarsContent) {
2673
+ const warnings = [];
2674
+ const parsedJsons = [];
2675
+ for (const file of files) {
2676
+ try {
2677
+ const json = await parseHclToJson(file.content, file.path);
2678
+ parsedJsons.push({ path: file.path, json });
2679
+ } catch (err) {
2680
+ const msg = err instanceof Error ? err.message : String(err);
2681
+ warnings.push(`Parse error in ${file.path}: ${msg}`);
2682
+ logger.warn("Skipping file due to parse error", {
2683
+ path: file.path,
2684
+ error: msg
2685
+ });
2686
+ }
2687
+ }
2688
+ if (parsedJsons.length === 0) {
2689
+ logger.warn("No files were successfully parsed");
2690
+ return {
2691
+ provider: "aws",
2692
+ region: PROVIDER_DEFAULTS["aws"],
2693
+ resources: [],
2694
+ total_count: 0,
2695
+ by_type: {},
2696
+ parse_warnings: warnings
2697
+ };
2698
+ }
2699
+ const combined = mergeHclJsons(parsedJsons.map((p) => p.json));
2700
+ const variables = resolveVariables(combined, tfvarsContent);
2701
+ const providers = ["aws", "azure", "gcp"];
2702
+ const defaultRegions = {};
2703
+ for (const p of providers) {
2704
+ defaultRegions[p] = detectRegionFromProviders(combined, p, variables);
2705
+ }
2706
+ const allResources = [];
2707
+ for (const { path, json } of parsedJsons) {
2708
+ try {
2709
+ const resources = extractResources(json, variables, path, defaultRegions, warnings);
2710
+ allResources.push(...resources);
2711
+ } catch (err) {
2712
+ const msg = err instanceof Error ? err.message : String(err);
2713
+ warnings.push(`Extraction error in ${path}: ${msg}`);
2714
+ logger.warn("Resource extraction failed", { path, error: msg });
2715
+ }
2716
+ }
2717
+ const dominantProvider = inferDominantProvider(allResources);
2718
+ const dominantRegion = defaultRegions[dominantProvider] ?? PROVIDER_DEFAULTS[dominantProvider];
2719
+ const byType = {};
2720
+ for (const r of allResources) {
2721
+ byType[r.type] = (byType[r.type] ?? 0) + 1;
2722
+ }
2723
+ const inventory = {
2724
+ provider: dominantProvider,
2725
+ region: dominantRegion,
2726
+ resources: allResources,
2727
+ total_count: allResources.length,
2728
+ by_type: byType,
2729
+ parse_warnings: warnings
2730
+ };
2731
+ logger.info("Terraform parse complete", {
2732
+ fileCount: files.length,
2733
+ resourceCount: allResources.length,
2734
+ provider: dominantProvider,
2735
+ region: dominantRegion,
2736
+ warningCount: warnings.length
2737
+ });
2738
+ return inventory;
2739
+ }
2740
+ function mergeHclJsons(jsons) {
2741
+ const merged = {};
2742
+ for (const json of jsons) {
2743
+ for (const [key, value] of Object.entries(json)) {
2744
+ if (!(key in merged)) {
2745
+ merged[key] = deepClone(value);
2746
+ continue;
2747
+ }
2748
+ const existing = merged[key];
2749
+ if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
2750
+ merged[key] = mergeObjects(
2751
+ existing,
2752
+ value
2753
+ );
2754
+ } else if (Array.isArray(existing) && Array.isArray(value)) {
2755
+ merged[key] = [...existing, ...value];
2756
+ } else {
2757
+ merged[key] = deepClone(value);
2758
+ }
2759
+ }
2760
+ }
2761
+ return merged;
2762
+ }
2763
+ function mergeObjects(a, b) {
2764
+ const result = { ...a };
2765
+ for (const [key, bVal] of Object.entries(b)) {
2766
+ const aVal = result[key];
2767
+ if (aVal !== null && typeof aVal === "object" && !Array.isArray(aVal) && bVal !== null && typeof bVal === "object" && !Array.isArray(bVal)) {
2768
+ result[key] = mergeObjects(
2769
+ aVal,
2770
+ bVal
2771
+ );
2772
+ } else if (Array.isArray(aVal) && Array.isArray(bVal)) {
2773
+ result[key] = [...aVal, ...bVal];
2774
+ } else {
2775
+ result[key] = deepClone(bVal);
2776
+ }
2777
+ }
2778
+ return result;
2779
+ }
2780
+ function deepClone(value) {
2781
+ if (value === null || typeof value !== "object") return value;
2782
+ return JSON.parse(JSON.stringify(value));
2783
+ }
2784
+
2785
+ // src/tools/analyze-terraform.ts
2786
+ var analyzeTerraformSchema = z.object({
2787
+ files: z.array(
2788
+ z.object({
2789
+ path: z.string().describe("File path"),
2790
+ content: z.string().describe("File content (HCL)")
2791
+ })
2792
+ ).describe("Terraform files to analyze"),
2793
+ tfvars: z.string().optional().describe("Contents of terraform.tfvars file")
2794
+ });
2795
+ async function analyzeTerraform(params) {
2796
+ const inventory = await parseTerraform(params.files, params.tfvars);
2797
+ const { parse_warnings, ...rest } = inventory;
2798
+ return {
2799
+ parse_warnings,
2800
+ has_warnings: parse_warnings.length > 0,
2801
+ ...rest
2802
+ };
2803
+ }
2804
+
2805
+ // src/tools/estimate-cost.ts
2806
+ import { z as z2 } from "zod";
2807
+
2808
+ // src/mapping/region-mapper.ts
2809
+ var DEFAULT_REGIONS = {
2810
+ aws: "us-east-1",
2811
+ azure: "eastus",
2812
+ gcp: "us-central1"
2813
+ };
2814
+ function mapRegion(region, sourceProvider, targetProvider) {
2815
+ if (sourceProvider === targetProvider) return region;
2816
+ const mappings = getRegionMappings();
2817
+ const row = mappings.find(
2818
+ (entry) => entry[sourceProvider] === region
2819
+ );
2820
+ if (!row) {
2821
+ const fallback = DEFAULT_REGIONS[targetProvider];
2822
+ logger.debug("region-mapper: no mapping found, using default", {
2823
+ region,
2824
+ sourceProvider,
2825
+ targetProvider,
2826
+ fallback
2827
+ });
2828
+ return fallback;
2829
+ }
2830
+ const mapped = row[targetProvider];
2831
+ if (!mapped) {
2832
+ return DEFAULT_REGIONS[targetProvider];
2833
+ }
2834
+ return mapped;
2835
+ }
2836
+
2837
+ // src/mapping/instance-mapper.ts
2838
+ function directionKey(source, target) {
2839
+ if (source === target) return null;
2840
+ return `${source}_to_${target}`;
2841
+ }
2842
+ function getSpecsForProvider(provider) {
2843
+ switch (provider) {
2844
+ case "aws":
2845
+ return getAwsInstances();
2846
+ case "azure":
2847
+ return getAzureVmSizes();
2848
+ case "gcp":
2849
+ return getGcpMachineTypes();
2850
+ }
2851
+ }
2852
+ var SCORE_EXACT_VCPU = 40;
2853
+ var SCORE_APPROX_VCPU = 20;
2854
+ var SCORE_EXACT_MEMORY = 40;
2855
+ var SCORE_APPROX_MEMORY = 20;
2856
+ var SCORE_CATEGORY = 20;
2857
+ function mapInstance(instanceType, sourceProvider, targetProvider) {
2858
+ if (sourceProvider === targetProvider) return instanceType;
2859
+ const key = directionKey(sourceProvider, targetProvider);
2860
+ if (!key) return instanceType;
2861
+ const instanceMap = getInstanceMap();
2862
+ const directionalMap = instanceMap[key];
2863
+ if (directionalMap) {
2864
+ const exact = directionalMap[instanceType];
2865
+ if (exact) {
2866
+ logger.debug("instance-mapper: exact match", {
2867
+ instanceType,
2868
+ sourceProvider,
2869
+ targetProvider,
2870
+ result: exact
2871
+ });
2872
+ return exact;
2873
+ }
2874
+ }
2875
+ const sourceSpecs = getSpecsForProvider(sourceProvider);
2876
+ const sourceSpec = sourceSpecs.find(
2877
+ (s) => s.instance_type === instanceType
2878
+ );
2879
+ if (!sourceSpec) {
2880
+ logger.debug("instance-mapper: source spec not found, cannot map", {
2881
+ instanceType,
2882
+ sourceProvider
2883
+ });
2884
+ return null;
2885
+ }
2886
+ return findNearestInstance(
2887
+ {
2888
+ vcpus: sourceSpec.vcpus,
2889
+ memory_gb: sourceSpec.memory_gb,
2890
+ category: sourceSpec.category
2891
+ },
2892
+ targetProvider
2893
+ );
2894
+ }
2895
+ function findNearestInstance(spec, targetProvider) {
2896
+ const candidates = getSpecsForProvider(targetProvider);
2897
+ if (candidates.length === 0) return null;
2898
+ let bestScore = -1;
2899
+ let bestType = null;
2900
+ for (const candidate of candidates) {
2901
+ let score = 0;
2902
+ if (candidate.vcpus === spec.vcpus) {
2903
+ score += SCORE_EXACT_VCPU;
2904
+ } else {
2905
+ const larger = Math.max(candidate.vcpus, spec.vcpus);
2906
+ const smaller = Math.min(candidate.vcpus, spec.vcpus);
2907
+ if (smaller > 0) {
2908
+ score += SCORE_APPROX_VCPU * (smaller / larger);
2909
+ }
2910
+ }
2911
+ if (candidate.memory_gb === spec.memory_gb) {
2912
+ score += SCORE_EXACT_MEMORY;
2913
+ } else {
2914
+ const larger = Math.max(candidate.memory_gb, spec.memory_gb);
2915
+ const smaller = Math.min(candidate.memory_gb, spec.memory_gb);
2916
+ if (smaller > 0) {
2917
+ score += SCORE_APPROX_MEMORY * (smaller / larger);
2918
+ }
2919
+ }
2920
+ if (candidate.category === spec.category) {
2921
+ score += SCORE_CATEGORY;
2922
+ }
2923
+ if (score > bestScore) {
2924
+ bestScore = score;
2925
+ bestType = candidate.instance_type;
2926
+ }
2927
+ }
2928
+ if (bestType) {
2929
+ logger.debug("instance-mapper: nearest match found", {
2930
+ spec,
2931
+ targetProvider,
2932
+ result: bestType,
2933
+ score: bestScore
2934
+ });
2935
+ }
2936
+ return bestType;
2937
+ }
2938
+
2939
+ // src/mapping/storage-mapper.ts
2940
+ function mapStorageType(storageType, sourceProvider, targetProvider) {
2941
+ if (sourceProvider === targetProvider) return storageType;
2942
+ const { block_storage, object_storage } = getStorageMap();
2943
+ const allEntries = [...block_storage, ...object_storage];
2944
+ const row = allEntries.find(
2945
+ (entry) => entry[sourceProvider] === storageType
2946
+ );
2947
+ if (!row) {
2948
+ logger.debug("storage-mapper: no mapping found", {
2949
+ storageType,
2950
+ sourceProvider,
2951
+ targetProvider
2952
+ });
2953
+ return null;
2954
+ }
2955
+ const mapped = row[targetProvider];
2956
+ if (!mapped) {
2957
+ logger.debug("storage-mapper: target column missing in row", {
2958
+ storageType,
2959
+ sourceProvider,
2960
+ targetProvider
2961
+ });
2962
+ return null;
2963
+ }
2964
+ return mapped;
2965
+ }
2966
+
2967
+ // src/calculator/compute.ts
2968
+ async function calculateComputeCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
2969
+ const notes = [];
2970
+ const breakdown = [];
2971
+ const sourceInstanceType = resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.machine_type ?? "";
2972
+ const mappedInstance = sourceInstanceType ? mapInstance(sourceInstanceType, resource.provider, targetProvider) : null;
2973
+ let confidence = "low";
2974
+ let specBasedMapping = false;
2975
+ if (!mappedInstance) {
2976
+ notes.push(
2977
+ `No instance mapping found for ${sourceInstanceType} (${resource.provider} -> ${targetProvider})`
2978
+ );
2979
+ } else {
2980
+ const { getInstanceMap: getInstanceMap2 } = await import("./loader-WIX54B7L.js");
2981
+ const im = getInstanceMap2();
2982
+ const dirKey = `${resource.provider}_to_${targetProvider}`;
2983
+ const directMap = im[dirKey];
2984
+ const isExact = directMap?.[sourceInstanceType] === mappedInstance;
2985
+ confidence = isExact ? "high" : "medium";
2986
+ if (!isExact) {
2987
+ specBasedMapping = true;
2988
+ notes.push(
2989
+ `Instance mapping for ${sourceInstanceType} -> ${mappedInstance} is an approximate spec-based match`
2990
+ );
2991
+ }
2992
+ }
2993
+ const effectiveInstance = mappedInstance ?? sourceInstanceType;
2994
+ const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(effectiveInstance, targetRegion, resource.attributes.os) : null;
2995
+ let computeMonthlyCost = 0;
2996
+ let pricingSource = "fallback";
2997
+ if (computePrice) {
2998
+ const rawSource = computePrice.attributes?.pricing_source;
2999
+ if (rawSource === "bundled") {
3000
+ pricingSource = "bundled";
3001
+ } else if (rawSource === "fallback") {
3002
+ pricingSource = "fallback";
3003
+ notes.push(`Pricing for ${effectiveInstance} uses bundled fallback data (live API unavailable)`);
3004
+ } else {
3005
+ pricingSource = "live";
3006
+ }
3007
+ if (specBasedMapping) {
3008
+ notes.push(`Spec-based instance mapping used: ${sourceInstanceType} -> ${mappedInstance}`);
3009
+ }
3010
+ const hourlyPrice = computePrice.price_per_unit;
3011
+ computeMonthlyCost = hourlyPrice * monthlyHours;
3012
+ breakdown.push({
3013
+ description: `${effectiveInstance} compute (${monthlyHours}h/month)`,
3014
+ unit: "Hrs",
3015
+ quantity: monthlyHours,
3016
+ unit_price: hourlyPrice,
3017
+ monthly_cost: computeMonthlyCost
3018
+ });
3019
+ } else {
3020
+ notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3021
+ confidence = "low";
3022
+ pricingSource = "fallback";
3023
+ }
3024
+ let storageMonthlyCost = 0;
3025
+ const rootDisk = resource.attributes.root_block_device;
3026
+ const storageType = rootDisk?.volume_type ?? resource.attributes.storage_type;
3027
+ const storageSizeGb = rootDisk?.volume_size ?? resource.attributes.storage_size_gb;
3028
+ if (storageType && storageSizeGb && storageSizeGb > 0) {
3029
+ const mappedStorageType = mapStorageType(
3030
+ storageType,
3031
+ resource.provider,
3032
+ targetProvider
3033
+ ) ?? storageType;
3034
+ const storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(mappedStorageType, targetRegion, storageSizeGb);
3035
+ if (storagePrice) {
3036
+ storageMonthlyCost = storagePrice.price_per_unit * storageSizeGb;
3037
+ breakdown.push({
3038
+ description: `Root disk ${mappedStorageType} (${storageSizeGb} GB)`,
3039
+ unit: "GB-Mo",
3040
+ quantity: storageSizeGb,
3041
+ unit_price: storagePrice.price_per_unit,
3042
+ monthly_cost: storageMonthlyCost
3043
+ });
3044
+ } else {
3045
+ logger.debug("calculateComputeCost: no storage price found", {
3046
+ mappedStorageType,
3047
+ targetRegion
3048
+ });
3049
+ }
3050
+ }
3051
+ const totalMonthly = computeMonthlyCost + storageMonthlyCost;
3052
+ return {
3053
+ resource_id: resource.id,
3054
+ resource_type: resource.type,
3055
+ resource_name: resource.name,
3056
+ provider: targetProvider,
3057
+ region: targetRegion,
3058
+ monthly_cost: totalMonthly,
3059
+ yearly_cost: totalMonthly * 12,
3060
+ currency: "USD",
3061
+ breakdown,
3062
+ confidence,
3063
+ notes,
3064
+ pricing_source: pricingSource
3065
+ };
3066
+ }
3067
+
3068
+ // src/calculator/database.ts
3069
+ function normalizeDbInstanceClass(instanceClass) {
3070
+ return instanceClass.startsWith("db.") ? instanceClass.slice(3) : instanceClass;
3071
+ }
3072
+ async function calculateDatabaseCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3073
+ const notes = [];
3074
+ const breakdown = [];
3075
+ const sourceInstanceClass = resource.attributes.instance_class ?? resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.tier ?? "";
3076
+ const isMultiAz = Boolean(resource.attributes.multi_az);
3077
+ const allocatedStorageGb = resource.attributes.allocated_storage ?? resource.attributes.storage_size_gb ?? 0;
3078
+ const engine = resource.attributes.engine ?? "MySQL";
3079
+ const normalised = normalizeDbInstanceClass(sourceInstanceClass);
3080
+ let mappedInstance = null;
3081
+ if (targetProvider === "gcp") {
3082
+ mappedInstance = mapInstance(sourceInstanceClass, resource.provider, "gcp");
3083
+ if (!mappedInstance) {
3084
+ mappedInstance = mapInstance(normalised, resource.provider, "gcp");
3085
+ }
3086
+ if (mappedInstance && !mappedInstance.startsWith("db-")) {
3087
+ const dbMapped = mapInstance(sourceInstanceClass, resource.provider, "gcp");
3088
+ if (dbMapped) mappedInstance = dbMapped;
3089
+ }
3090
+ } else if (targetProvider === "aws") {
3091
+ mappedInstance = mapInstance(sourceInstanceClass, resource.provider, "aws");
3092
+ if (!mappedInstance && normalised !== sourceInstanceClass) {
3093
+ mappedInstance = mapInstance(normalised, resource.provider, "aws");
3094
+ }
3095
+ if (mappedInstance && !mappedInstance.startsWith("db.")) {
3096
+ mappedInstance = `db.${mappedInstance}`;
3097
+ }
3098
+ } else {
3099
+ mappedInstance = mapInstance(sourceInstanceClass, resource.provider, targetProvider);
3100
+ if (!mappedInstance) {
3101
+ mappedInstance = mapInstance(normalised, resource.provider, targetProvider);
3102
+ }
3103
+ }
3104
+ const effectiveInstance = mappedInstance ?? sourceInstanceClass;
3105
+ if (!mappedInstance) {
3106
+ notes.push(
3107
+ `No DB instance mapping found for ${sourceInstanceClass} (${resource.provider} -> ${targetProvider}); using source class`
3108
+ );
3109
+ }
3110
+ const dbPrice = await pricingEngine.getProvider(targetProvider).getDatabasePrice(effectiveInstance, targetRegion, engine);
3111
+ let instanceMonthlyCost = 0;
3112
+ let pricingSource = "fallback";
3113
+ if (dbPrice) {
3114
+ const rawSource = dbPrice.attributes?.pricing_source;
3115
+ if (rawSource === "bundled") {
3116
+ pricingSource = "bundled";
3117
+ } else if (rawSource === "fallback") {
3118
+ pricingSource = "fallback";
3119
+ notes.push(`Pricing for ${effectiveInstance} uses bundled fallback data (live API unavailable)`);
3120
+ } else {
3121
+ pricingSource = "live";
3122
+ }
3123
+ instanceMonthlyCost = dbPrice.price_per_unit * monthlyHours;
3124
+ let haMultiplier = 1;
3125
+ if (isMultiAz) {
3126
+ if (targetProvider === "gcp") {
3127
+ const gcpMult = parseFloat(
3128
+ dbPrice.attributes?.ha_multiplier ?? "2"
3129
+ );
3130
+ haMultiplier = isNaN(gcpMult) ? 2 : gcpMult;
3131
+ } else {
3132
+ haMultiplier = 2;
3133
+ }
3134
+ notes.push(`Multi-AZ enabled: instance cost multiplied by ${haMultiplier}`);
3135
+ }
3136
+ const adjustedCost = instanceMonthlyCost * haMultiplier;
3137
+ breakdown.push({
3138
+ description: `${effectiveInstance} DB instance (${monthlyHours}h/month)${isMultiAz ? " x" + haMultiplier + " (Multi-AZ)" : ""}`,
3139
+ unit: "Hrs",
3140
+ quantity: monthlyHours * haMultiplier,
3141
+ unit_price: dbPrice.price_per_unit,
3142
+ monthly_cost: adjustedCost
3143
+ });
3144
+ instanceMonthlyCost = adjustedCost;
3145
+ } else {
3146
+ notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3147
+ logger.debug("calculateDatabaseCost: no price", {
3148
+ effectiveInstance,
3149
+ targetRegion
3150
+ });
3151
+ }
3152
+ let storageMonthlyCost = 0;
3153
+ if (allocatedStorageGb > 0) {
3154
+ const storageType = resource.attributes.storage_type ?? "gp3";
3155
+ const storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(storageType, targetRegion, allocatedStorageGb);
3156
+ if (storagePrice) {
3157
+ storageMonthlyCost = storagePrice.price_per_unit * allocatedStorageGb;
3158
+ breakdown.push({
3159
+ description: `DB storage ${storageType} (${allocatedStorageGb} GB)`,
3160
+ unit: "GB-Mo",
3161
+ quantity: allocatedStorageGb,
3162
+ unit_price: storagePrice.price_per_unit,
3163
+ monthly_cost: storageMonthlyCost
3164
+ });
3165
+ }
3166
+ }
3167
+ const totalMonthly = instanceMonthlyCost + storageMonthlyCost;
3168
+ return {
3169
+ resource_id: resource.id,
3170
+ resource_type: resource.type,
3171
+ resource_name: resource.name,
3172
+ provider: targetProvider,
3173
+ region: targetRegion,
3174
+ monthly_cost: totalMonthly,
3175
+ yearly_cost: totalMonthly * 12,
3176
+ currency: "USD",
3177
+ breakdown,
3178
+ confidence: mappedInstance ? "high" : "low",
3179
+ notes,
3180
+ pricing_source: pricingSource
3181
+ };
3182
+ }
3183
+
3184
+ // src/calculator/storage.ts
3185
+ var DEFAULT_OBJECT_STORAGE_GB = 100;
3186
+ var BLOCK_STORAGE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3187
+ "aws_ebs_volume",
3188
+ "azurerm_managed_disk",
3189
+ "google_compute_disk"
3190
+ ]);
3191
+ var BLOCK_STORAGE_FALLBACK = {
3192
+ aws: "gp3",
3193
+ azure: "Premium_LRS",
3194
+ gcp: "pd-ssd"
3195
+ };
3196
+ function isBlockStorage(resource) {
3197
+ return BLOCK_STORAGE_RESOURCE_TYPES.has(resource.type);
3198
+ }
3199
+ async function calculateStorageCost(resource, targetProvider, targetRegion, pricingEngine) {
3200
+ const notes = [];
3201
+ const breakdown = [];
3202
+ const block = isBlockStorage(resource);
3203
+ const sourceStorageType = resource.attributes.storage_type ?? resource.attributes.type ?? resource.attributes.storage_class ?? (block ? "gp3" : "STANDARD");
3204
+ const mappedStorageType = mapStorageType(sourceStorageType, resource.provider, targetProvider) ?? sourceStorageType;
3205
+ let sizeGb = resource.attributes.storage_size_gb ?? resource.attributes.disk_size_gb ?? resource.attributes.size;
3206
+ if (!sizeGb || sizeGb <= 0) {
3207
+ if (block) {
3208
+ sizeGb = 8;
3209
+ notes.push("No storage size specified; defaulting to 8 GB for block volume");
3210
+ } else {
3211
+ sizeGb = DEFAULT_OBJECT_STORAGE_GB;
3212
+ notes.push(
3213
+ `No storage size specified; assuming ${DEFAULT_OBJECT_STORAGE_GB} GB for object storage cost estimate`
3214
+ );
3215
+ }
3216
+ }
3217
+ let resolvedStorageType = mappedStorageType;
3218
+ let storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(resolvedStorageType, targetRegion, sizeGb);
3219
+ if (!storagePrice && !block) {
3220
+ const fallbackType = BLOCK_STORAGE_FALLBACK[targetProvider];
3221
+ if (fallbackType) {
3222
+ storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(fallbackType, targetRegion, sizeGb);
3223
+ if (storagePrice) {
3224
+ resolvedStorageType = fallbackType;
3225
+ notes.push(
3226
+ `Object storage class "${mappedStorageType}" not directly priced; using ${fallbackType} rate as an approximation`
3227
+ );
3228
+ }
3229
+ }
3230
+ }
3231
+ let totalMonthly = 0;
3232
+ let pricingSource = "fallback";
3233
+ if (storagePrice) {
3234
+ const rawSource = storagePrice.attributes?.pricing_source;
3235
+ if (rawSource === "bundled") {
3236
+ pricingSource = "bundled";
3237
+ } else if (rawSource === "fallback") {
3238
+ pricingSource = "fallback";
3239
+ notes.push(`Pricing for ${resolvedStorageType} uses bundled fallback data (live API unavailable)`);
3240
+ } else {
3241
+ pricingSource = "live";
3242
+ }
3243
+ totalMonthly = storagePrice.price_per_unit * sizeGb;
3244
+ breakdown.push({
3245
+ description: `${block ? "Block" : "Object"} storage ${resolvedStorageType} (${sizeGb} GB)`,
3246
+ unit: "GB-Mo",
3247
+ quantity: sizeGb,
3248
+ unit_price: storagePrice.price_per_unit,
3249
+ monthly_cost: totalMonthly
3250
+ });
3251
+ } else {
3252
+ notes.push(
3253
+ `No pricing data found for ${resource.type} in ${targetRegion}`
3254
+ );
3255
+ logger.debug("calculateStorageCost: no price", {
3256
+ mappedStorageType,
3257
+ targetRegion
3258
+ });
3259
+ }
3260
+ return {
3261
+ resource_id: resource.id,
3262
+ resource_type: resource.type,
3263
+ resource_name: resource.name,
3264
+ provider: targetProvider,
3265
+ region: targetRegion,
3266
+ monthly_cost: totalMonthly,
3267
+ yearly_cost: totalMonthly * 12,
3268
+ currency: "USD",
3269
+ breakdown,
3270
+ confidence: storagePrice ? "high" : "low",
3271
+ notes,
3272
+ pricing_source: pricingSource
3273
+ };
3274
+ }
3275
+
3276
+ // src/calculator/network.ts
3277
+ var DEFAULT_NAT_DATA_GB = 100;
3278
+ async function calculateNatGatewayCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3279
+ const notes = [];
3280
+ const breakdown = [];
3281
+ const natPrice = await pricingEngine.getProvider(targetProvider).getNatGatewayPrice(targetRegion);
3282
+ let totalMonthly = 0;
3283
+ let natPricingSource = "fallback";
3284
+ if (natPrice) {
3285
+ const rawSource = natPrice.attributes?.pricing_source;
3286
+ if (rawSource === "bundled") {
3287
+ natPricingSource = "bundled";
3288
+ } else if (rawSource === "fallback") {
3289
+ natPricingSource = "fallback";
3290
+ } else {
3291
+ natPricingSource = "live";
3292
+ }
3293
+ const hourlyCharge = natPrice.price_per_unit * monthlyHours;
3294
+ breakdown.push({
3295
+ description: `NAT Gateway hourly charge (${monthlyHours}h/month)`,
3296
+ unit: "Hrs",
3297
+ quantity: monthlyHours,
3298
+ unit_price: natPrice.price_per_unit,
3299
+ monthly_cost: hourlyCharge
3300
+ });
3301
+ totalMonthly += hourlyCharge;
3302
+ const perGbPrice = parseFloat(
3303
+ natPrice.attributes?.per_gb_price ?? "0"
3304
+ );
3305
+ if (perGbPrice > 0) {
3306
+ const dataGb = resource.attributes.data_processed_gb ?? DEFAULT_NAT_DATA_GB;
3307
+ if (!resource.attributes.data_processed_gb || resource.attributes.data_processed_gb <= 0) {
3308
+ notes.push(
3309
+ `Data processing cost estimated at ${dataGb} GB/month; provide data_processed_gb attribute for accuracy`
3310
+ );
3311
+ }
3312
+ const dataCharge = perGbPrice * dataGb;
3313
+ breakdown.push({
3314
+ description: `NAT Gateway data processing (${dataGb} GB/month)`,
3315
+ unit: "GB",
3316
+ quantity: dataGb,
3317
+ unit_price: perGbPrice,
3318
+ monthly_cost: dataCharge
3319
+ });
3320
+ totalMonthly += dataCharge;
3321
+ }
3322
+ } else {
3323
+ notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3324
+ }
3325
+ return {
3326
+ resource_id: resource.id,
3327
+ resource_type: resource.type,
3328
+ resource_name: resource.name,
3329
+ provider: targetProvider,
3330
+ region: targetRegion,
3331
+ monthly_cost: totalMonthly,
3332
+ yearly_cost: totalMonthly * 12,
3333
+ currency: "USD",
3334
+ breakdown,
3335
+ confidence: natPrice ? "medium" : "low",
3336
+ notes,
3337
+ pricing_source: natPricingSource
3338
+ };
3339
+ }
3340
+ async function calculateLoadBalancerCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3341
+ const notes = [];
3342
+ const breakdown = [];
3343
+ const lbType = resource.attributes.load_balancer_type ?? "application";
3344
+ const lbPrice = await pricingEngine.getProvider(targetProvider).getLoadBalancerPrice(lbType, targetRegion);
3345
+ let totalMonthly = 0;
3346
+ let lbPricingSource = "fallback";
3347
+ if (lbPrice) {
3348
+ const rawSource = lbPrice.attributes?.pricing_source;
3349
+ if (rawSource === "bundled") {
3350
+ lbPricingSource = "bundled";
3351
+ } else if (rawSource === "fallback") {
3352
+ lbPricingSource = "fallback";
3353
+ } else {
3354
+ lbPricingSource = "live";
3355
+ }
3356
+ totalMonthly = lbPrice.price_per_unit * monthlyHours;
3357
+ breakdown.push({
3358
+ description: `Load Balancer hourly charge (${monthlyHours}h/month)`,
3359
+ unit: "Hrs",
3360
+ quantity: monthlyHours,
3361
+ unit_price: lbPrice.price_per_unit,
3362
+ monthly_cost: totalMonthly
3363
+ });
3364
+ notes.push(
3365
+ "Traffic-based charges (LCU/data processing) are excluded; this is the fixed hourly component only"
3366
+ );
3367
+ } else {
3368
+ notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3369
+ }
3370
+ return {
3371
+ resource_id: resource.id,
3372
+ resource_type: resource.type,
3373
+ resource_name: resource.name,
3374
+ provider: targetProvider,
3375
+ region: targetRegion,
3376
+ monthly_cost: totalMonthly,
3377
+ yearly_cost: totalMonthly * 12,
3378
+ currency: "USD",
3379
+ breakdown,
3380
+ confidence: lbPrice ? "medium" : "low",
3381
+ notes,
3382
+ pricing_source: lbPricingSource
3383
+ };
3384
+ }
3385
+
3386
+ // src/calculator/kubernetes.ts
3387
+ async function calculateKubernetesCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3388
+ const notes = [];
3389
+ const breakdown = [];
3390
+ const k8sPrice = await pricingEngine.getProvider(targetProvider).getKubernetesPrice(targetRegion);
3391
+ let totalMonthly = 0;
3392
+ let pricingSource = "fallback";
3393
+ if (k8sPrice) {
3394
+ const rawSource = k8sPrice.attributes?.pricing_source;
3395
+ if (rawSource === "bundled") {
3396
+ pricingSource = "bundled";
3397
+ } else if (rawSource === "fallback") {
3398
+ pricingSource = "fallback";
3399
+ } else {
3400
+ pricingSource = "live";
3401
+ }
3402
+ const controlPlaneCost = k8sPrice.price_per_unit * monthlyHours;
3403
+ breakdown.push({
3404
+ description: `Kubernetes control plane (${monthlyHours}h/month)`,
3405
+ unit: "Hrs",
3406
+ quantity: monthlyHours,
3407
+ unit_price: k8sPrice.price_per_unit,
3408
+ monthly_cost: controlPlaneCost
3409
+ });
3410
+ totalMonthly += controlPlaneCost;
3411
+ } else {
3412
+ notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3413
+ }
3414
+ const nodeCount = resource.attributes.node_count ?? resource.attributes.min_node_count;
3415
+ if (nodeCount && nodeCount > 0) {
3416
+ const nodeInstanceType = resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.machine_type;
3417
+ if (nodeInstanceType) {
3418
+ const nodeResource = {
3419
+ ...resource,
3420
+ id: `${resource.id}/node`,
3421
+ type: "compute_node",
3422
+ name: `${resource.name}-node`,
3423
+ attributes: {
3424
+ instance_type: nodeInstanceType,
3425
+ vm_size: nodeInstanceType,
3426
+ machine_type: nodeInstanceType
3427
+ }
3428
+ };
3429
+ const nodeCostEstimate = await calculateComputeCost(
3430
+ nodeResource,
3431
+ targetProvider,
3432
+ targetRegion,
3433
+ pricingEngine,
3434
+ monthlyHours
3435
+ );
3436
+ if (nodeCostEstimate.monthly_cost > 0) {
3437
+ const nodeGroupCost = nodeCostEstimate.monthly_cost * nodeCount;
3438
+ breakdown.push({
3439
+ description: `${nodeCount}x node (${nodeInstanceType}) compute`,
3440
+ unit: "nodes",
3441
+ quantity: nodeCount,
3442
+ unit_price: nodeCostEstimate.monthly_cost,
3443
+ monthly_cost: nodeGroupCost
3444
+ });
3445
+ totalMonthly += nodeGroupCost;
3446
+ notes.push(
3447
+ `Node cost estimated for ${nodeCount} nodes of type ${nodeInstanceType}`
3448
+ );
3449
+ }
3450
+ } else {
3451
+ notes.push(
3452
+ `node_count=${nodeCount} specified but no instance_type found; node compute cost excluded`
3453
+ );
3454
+ }
3455
+ }
3456
+ return {
3457
+ resource_id: resource.id,
3458
+ resource_type: resource.type,
3459
+ resource_name: resource.name,
3460
+ provider: targetProvider,
3461
+ region: targetRegion,
3462
+ monthly_cost: totalMonthly,
3463
+ yearly_cost: totalMonthly * 12,
3464
+ currency: "USD",
3465
+ breakdown,
3466
+ confidence: k8sPrice ? "medium" : "low",
3467
+ notes,
3468
+ pricing_source: pricingSource
3469
+ };
3470
+ }
3471
+
3472
+ // src/calculator/cost-engine.ts
3473
+ var COMPUTE_TYPES = /* @__PURE__ */ new Set([
3474
+ "aws_instance",
3475
+ "azurerm_linux_virtual_machine",
3476
+ "azurerm_windows_virtual_machine",
3477
+ "google_compute_instance",
3478
+ // Node groups – cost their compute like VMs.
3479
+ "aws_eks_node_group",
3480
+ "azurerm_kubernetes_cluster_node_pool",
3481
+ "google_container_node_pool",
3482
+ "compute_node"
3483
+ ]);
3484
+ var DATABASE_TYPES = /* @__PURE__ */ new Set([
3485
+ "aws_db_instance",
3486
+ "azurerm_postgresql_flexible_server",
3487
+ "azurerm_mysql_flexible_server",
3488
+ "azurerm_mssql_server",
3489
+ "google_sql_database_instance"
3490
+ ]);
3491
+ var BLOCK_STORAGE_TYPES = /* @__PURE__ */ new Set([
3492
+ "aws_ebs_volume",
3493
+ "azurerm_managed_disk",
3494
+ "google_compute_disk"
3495
+ ]);
3496
+ var OBJECT_STORAGE_TYPES = /* @__PURE__ */ new Set([
3497
+ "aws_s3_bucket",
3498
+ "azurerm_storage_account",
3499
+ "google_storage_bucket"
3500
+ ]);
3501
+ var NAT_GATEWAY_TYPES = /* @__PURE__ */ new Set([
3502
+ "aws_nat_gateway",
3503
+ "azurerm_nat_gateway",
3504
+ "google_compute_router_nat"
3505
+ ]);
3506
+ var LOAD_BALANCER_TYPES = /* @__PURE__ */ new Set([
3507
+ "aws_lb",
3508
+ "aws_alb",
3509
+ "azurerm_lb",
3510
+ "azurerm_application_gateway",
3511
+ "google_compute_forwarding_rule"
3512
+ ]);
3513
+ var KUBERNETES_TYPES = /* @__PURE__ */ new Set([
3514
+ "aws_eks_cluster",
3515
+ "azurerm_kubernetes_cluster",
3516
+ "google_container_cluster"
3517
+ ]);
3518
+ function serviceLabel(resourceType) {
3519
+ if (COMPUTE_TYPES.has(resourceType)) return "compute";
3520
+ if (DATABASE_TYPES.has(resourceType)) return "database";
3521
+ if (BLOCK_STORAGE_TYPES.has(resourceType)) return "block_storage";
3522
+ if (OBJECT_STORAGE_TYPES.has(resourceType)) return "object_storage";
3523
+ if (NAT_GATEWAY_TYPES.has(resourceType)) return "network";
3524
+ if (LOAD_BALANCER_TYPES.has(resourceType)) return "network";
3525
+ if (KUBERNETES_TYPES.has(resourceType)) return "kubernetes";
3526
+ return "other";
3527
+ }
3528
+ var CostEngine = class {
3529
+ pricingEngine;
3530
+ monthlyHours;
3531
+ constructor(pricingEngine, config) {
3532
+ this.pricingEngine = pricingEngine;
3533
+ this.monthlyHours = config.pricing.monthly_hours;
3534
+ }
3535
+ /**
3536
+ * Calculates the cost of a single resource on a target provider.
3537
+ */
3538
+ async calculateCost(resource, targetProvider, targetRegion) {
3539
+ const type = resource.type;
3540
+ logger.debug("CostEngine.calculateCost", {
3541
+ resourceId: resource.id,
3542
+ type,
3543
+ targetProvider,
3544
+ targetRegion
3545
+ });
3546
+ if (COMPUTE_TYPES.has(type)) {
3547
+ return calculateComputeCost(
3548
+ resource,
3549
+ targetProvider,
3550
+ targetRegion,
3551
+ this.pricingEngine,
3552
+ this.monthlyHours
3553
+ );
3554
+ }
3555
+ if (DATABASE_TYPES.has(type)) {
3556
+ return calculateDatabaseCost(
3557
+ resource,
3558
+ targetProvider,
3559
+ targetRegion,
3560
+ this.pricingEngine,
3561
+ this.monthlyHours
3562
+ );
3563
+ }
3564
+ if (BLOCK_STORAGE_TYPES.has(type) || OBJECT_STORAGE_TYPES.has(type)) {
3565
+ return calculateStorageCost(
3566
+ resource,
3567
+ targetProvider,
3568
+ targetRegion,
3569
+ this.pricingEngine
3570
+ );
3571
+ }
3572
+ if (NAT_GATEWAY_TYPES.has(type)) {
3573
+ return calculateNatGatewayCost(
3574
+ resource,
3575
+ targetProvider,
3576
+ targetRegion,
3577
+ this.pricingEngine,
3578
+ this.monthlyHours
3579
+ );
3580
+ }
3581
+ if (LOAD_BALANCER_TYPES.has(type)) {
3582
+ return calculateLoadBalancerCost(
3583
+ resource,
3584
+ targetProvider,
3585
+ targetRegion,
3586
+ this.pricingEngine,
3587
+ this.monthlyHours
3588
+ );
3589
+ }
3590
+ if (KUBERNETES_TYPES.has(type)) {
3591
+ return calculateKubernetesCost(
3592
+ resource,
3593
+ targetProvider,
3594
+ targetRegion,
3595
+ this.pricingEngine,
3596
+ this.monthlyHours
3597
+ );
3598
+ }
3599
+ logger.warn("CostEngine: unsupported resource type", { type, targetProvider });
3600
+ return {
3601
+ resource_id: resource.id,
3602
+ resource_type: type,
3603
+ resource_name: resource.name,
3604
+ provider: targetProvider,
3605
+ region: targetRegion,
3606
+ monthly_cost: 0,
3607
+ yearly_cost: 0,
3608
+ currency: "USD",
3609
+ breakdown: [],
3610
+ confidence: "low",
3611
+ notes: [`Resource type "${type}" is not yet supported by the cost engine`],
3612
+ pricing_source: "fallback"
3613
+ };
3614
+ }
3615
+ /**
3616
+ * Calculates costs for all resources and aggregates them into a CostBreakdown.
3617
+ *
3618
+ * Resources that fail to produce a cost estimate are logged and skipped rather
3619
+ * than letting a single failure abort the entire calculation.
3620
+ */
3621
+ async calculateBreakdown(resources, targetProvider, targetRegion) {
3622
+ const estimates = [];
3623
+ const byService = {};
3624
+ const warnings = [];
3625
+ for (const resource of resources) {
3626
+ try {
3627
+ const estimate = await this.calculateCost(
3628
+ resource,
3629
+ targetProvider,
3630
+ targetRegion
3631
+ );
3632
+ estimates.push(estimate);
3633
+ const svc = serviceLabel(resource.type);
3634
+ byService[svc] = (byService[svc] ?? 0) + estimate.monthly_cost;
3635
+ if (estimate.pricing_source === "fallback") {
3636
+ warnings.push(
3637
+ `${estimate.resource_name} (${estimate.resource_type}): using fallback/bundled pricing data`
3638
+ );
3639
+ }
3640
+ if (estimate.monthly_cost === 0 && estimate.confidence === "low") {
3641
+ warnings.push(
3642
+ `No pricing data found for ${estimate.resource_type} in ${targetRegion} \u2014 cost reported as $0`
3643
+ );
3644
+ }
3645
+ } catch (err) {
3646
+ logger.error("CostEngine: failed to calculate cost for resource", {
3647
+ resourceId: resource.id,
3648
+ type: resource.type,
3649
+ error: err instanceof Error ? err.message : String(err)
3650
+ });
3651
+ }
3652
+ }
3653
+ const totalMonthly = estimates.reduce((sum, e) => sum + e.monthly_cost, 0);
3654
+ return {
3655
+ provider: targetProvider,
3656
+ region: targetRegion,
3657
+ total_monthly: Math.round(totalMonthly * 100) / 100,
3658
+ total_yearly: Math.round(totalMonthly * 12 * 100) / 100,
3659
+ currency: "USD",
3660
+ by_service: byService,
3661
+ by_resource: estimates,
3662
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3663
+ warnings
3664
+ };
3665
+ }
3666
+ };
3667
+
3668
+ // src/tools/estimate-cost.ts
3669
+ var estimateCostSchema = z2.object({
3670
+ files: z2.array(
3671
+ z2.object({
3672
+ path: z2.string().describe("File path"),
3673
+ content: z2.string().describe("File content (HCL)")
3674
+ })
3675
+ ),
3676
+ tfvars: z2.string().optional().describe("Contents of terraform.tfvars file"),
3677
+ provider: z2.enum(["aws", "azure", "gcp"]).describe("Target cloud provider to estimate costs for"),
3678
+ region: z2.string().optional().describe(
3679
+ "Target region for pricing lookup. Defaults to the source region mapped to the target provider."
3680
+ )
3681
+ });
3682
+ async function estimateCost(params, pricingEngine, config) {
3683
+ const inventory = await parseTerraform(params.files, params.tfvars);
3684
+ const targetProvider = params.provider;
3685
+ const targetRegion = params.region ?? mapRegion(inventory.region, inventory.provider, targetProvider);
3686
+ const costEngine = new CostEngine(pricingEngine, config);
3687
+ const breakdown = await costEngine.calculateBreakdown(
3688
+ inventory.resources,
3689
+ targetProvider,
3690
+ targetRegion
3691
+ );
3692
+ const parseWarnings = inventory.parse_warnings ?? [];
3693
+ const allWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...breakdown.warnings ?? []])];
3694
+ return {
3695
+ ...breakdown,
3696
+ parse_warnings: parseWarnings,
3697
+ warnings: allWarnings
3698
+ };
3699
+ }
3700
+
3701
+ // src/tools/compare-providers.ts
3702
+ import { z as z3 } from "zod";
3703
+
3704
+ // src/calculator/reserved.ts
3705
+ var DISCOUNT_RATES = {
3706
+ aws: [
3707
+ { term: "1yr", payment: "no_upfront", rate: 0.36 },
3708
+ { term: "1yr", payment: "partial_upfront", rate: 0.4 },
3709
+ { term: "1yr", payment: "all_upfront", rate: 0.42 },
3710
+ { term: "3yr", payment: "no_upfront", rate: 0.52 },
3711
+ { term: "3yr", payment: "partial_upfront", rate: 0.57 },
3712
+ { term: "3yr", payment: "all_upfront", rate: 0.6 }
3713
+ ],
3714
+ azure: [
3715
+ { term: "1yr", payment: "all_upfront", rate: 0.35 },
3716
+ { term: "3yr", payment: "all_upfront", rate: 0.55 },
3717
+ // Azure does not offer no-upfront RI in the same way; partial_upfront maps
3718
+ // to hybrid benefit combined, approximated here.
3719
+ { term: "1yr", payment: "partial_upfront", rate: 0.3 },
3720
+ { term: "3yr", payment: "partial_upfront", rate: 0.5 }
3721
+ ],
3722
+ gcp: [
3723
+ // GCP Committed Use Discounts (CUDs) – resource-based.
3724
+ { term: "1yr", payment: "all_upfront", rate: 0.28 },
3725
+ { term: "3yr", payment: "all_upfront", rate: 0.55 },
3726
+ { term: "1yr", payment: "partial_upfront", rate: 0.2 },
3727
+ { term: "3yr", payment: "partial_upfront", rate: 0.45 }
3728
+ ]
3729
+ };
3730
+ function calculateReservedPricing(onDemandMonthly, provider) {
3731
+ const rates = DISCOUNT_RATES[provider];
3732
+ const options = rates.map(({ term, payment, rate }) => {
3733
+ const monthly_cost = onDemandMonthly * (1 - rate);
3734
+ const monthly_savings = onDemandMonthly - monthly_cost;
3735
+ return {
3736
+ term,
3737
+ payment,
3738
+ monthly_cost: Math.round(monthly_cost * 100) / 100,
3739
+ monthly_savings: Math.round(monthly_savings * 100) / 100,
3740
+ percentage_savings: Math.round(rate * 1e3) / 10
3741
+ // e.g. 36.0
3742
+ };
3743
+ });
3744
+ const best_option = options.reduce(
3745
+ (best, opt) => opt.monthly_savings > best.monthly_savings ? opt : best,
3746
+ options[0]
3747
+ );
3748
+ return {
3749
+ on_demand_monthly: Math.round(onDemandMonthly * 100) / 100,
3750
+ options,
3751
+ best_option
3752
+ };
3753
+ }
3754
+
3755
+ // src/calculator/optimizer.ts
3756
+ var LARGE_INSTANCE_SUFFIXES = [
3757
+ ".xlarge",
3758
+ ".2xlarge",
3759
+ ".4xlarge",
3760
+ ".8xlarge",
3761
+ ".16xlarge",
3762
+ "Standard_B4ms",
3763
+ "Standard_B8ms",
3764
+ "Standard_D4",
3765
+ "Standard_D8",
3766
+ "Standard_D16",
3767
+ "Standard_E4",
3768
+ "Standard_E8",
3769
+ "n2-standard-4",
3770
+ "n2-standard-8",
3771
+ "n2-standard-16",
3772
+ "e2-standard-4",
3773
+ "e2-standard-8"
3774
+ ];
3775
+ var HIGH_UTILISATION_INDICATORS = [
3776
+ "high-cpu",
3777
+ "high-memory",
3778
+ "production",
3779
+ "prod",
3780
+ "perf",
3781
+ "performance"
3782
+ ];
3783
+ function isLikelyOversized(resource) {
3784
+ const instanceType = resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.machine_type ?? "";
3785
+ const isLarge = LARGE_INSTANCE_SUFFIXES.some(
3786
+ (suffix) => instanceType.toLowerCase().includes(suffix.toLowerCase())
3787
+ );
3788
+ if (!isLarge) return false;
3789
+ const tagValues = Object.values(resource.tags).map((v) => v.toLowerCase());
3790
+ const tagKeys = Object.keys(resource.tags).map((k) => k.toLowerCase());
3791
+ const allTagText = [...tagKeys, ...tagValues].join(" ");
3792
+ const hasHighUtilisationSignal = HIGH_UTILISATION_INDICATORS.some(
3793
+ (signal) => allTagText.includes(signal)
3794
+ );
3795
+ return !hasHighUtilisationSignal;
3796
+ }
3797
+ var COMPUTE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3798
+ "aws_instance",
3799
+ "azurerm_linux_virtual_machine",
3800
+ "azurerm_windows_virtual_machine",
3801
+ "google_compute_instance",
3802
+ "aws_eks_cluster",
3803
+ "aws_eks_node_group",
3804
+ "azurerm_kubernetes_cluster",
3805
+ "google_container_cluster",
3806
+ "google_container_node_pool"
3807
+ ]);
3808
+ var DATABASE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3809
+ "aws_db_instance",
3810
+ "azurerm_postgresql_flexible_server",
3811
+ "azurerm_mysql_flexible_server",
3812
+ "google_sql_database_instance"
3813
+ ]);
3814
+ var OBJECT_STORAGE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3815
+ "aws_s3_bucket",
3816
+ "azurerm_storage_account",
3817
+ "google_storage_bucket"
3818
+ ]);
3819
+ function generateOptimizations(estimates, resources) {
3820
+ const recommendations = [];
3821
+ const resourceById = new Map(
3822
+ resources.map((r) => [r.id, r])
3823
+ );
3824
+ const estimatesByResourceId = /* @__PURE__ */ new Map();
3825
+ for (const estimate of estimates) {
3826
+ const existing = estimatesByResourceId.get(estimate.resource_id) ?? [];
3827
+ existing.push(estimate);
3828
+ estimatesByResourceId.set(estimate.resource_id, existing);
3829
+ }
3830
+ for (const [resourceId, resourceEstimates] of estimatesByResourceId) {
3831
+ const resource = resourceById.get(resourceId);
3832
+ if (!resourceEstimates.length) continue;
3833
+ const current = resourceEstimates[0];
3834
+ if (resource && COMPUTE_RESOURCE_TYPES.has(resource.type)) {
3835
+ if (isLikelyOversized(resource) && current.monthly_cost > 0) {
3836
+ const estimatedSavings = current.monthly_cost * 0.3;
3837
+ recommendations.push({
3838
+ resource_id: resourceId,
3839
+ resource_name: current.resource_name,
3840
+ type: "right_size",
3841
+ description: `${resource.attributes.instance_type ?? resource.attributes.vm_size ?? "Instance"} appears over-provisioned. Consider downsizing to the next smaller instance type.`,
3842
+ current_monthly_cost: current.monthly_cost,
3843
+ estimated_monthly_cost: current.monthly_cost - estimatedSavings,
3844
+ monthly_savings: estimatedSavings,
3845
+ percentage_savings: 30,
3846
+ confidence: "medium",
3847
+ provider: current.provider
3848
+ });
3849
+ }
3850
+ }
3851
+ if (resource && (COMPUTE_RESOURCE_TYPES.has(resource.type) || DATABASE_RESOURCE_TYPES.has(resource.type)) && current.monthly_cost > 0) {
3852
+ const reserved = calculateReservedPricing(
3853
+ current.monthly_cost,
3854
+ current.provider
3855
+ );
3856
+ const best = reserved.best_option;
3857
+ if (best.percentage_savings >= 20) {
3858
+ recommendations.push({
3859
+ resource_id: resourceId,
3860
+ resource_name: current.resource_name,
3861
+ type: "reserved",
3862
+ description: `Switching to a ${best.term} ${best.payment.replace(/_/g, " ")} commitment could save approximately ${best.percentage_savings}% per month.`,
3863
+ current_monthly_cost: current.monthly_cost,
3864
+ estimated_monthly_cost: best.monthly_cost,
3865
+ monthly_savings: best.monthly_savings,
3866
+ percentage_savings: best.percentage_savings,
3867
+ confidence: "high",
3868
+ provider: current.provider
3869
+ });
3870
+ }
3871
+ }
3872
+ if (resourceEstimates.length > 1 && current.monthly_cost > 0) {
3873
+ const cheapest = resourceEstimates.reduce(
3874
+ (min, e) => e.monthly_cost < min.monthly_cost ? e : min,
3875
+ current
3876
+ );
3877
+ if (cheapest.provider !== current.provider) {
3878
+ const savings = current.monthly_cost - cheapest.monthly_cost;
3879
+ const pct = savings / current.monthly_cost * 100;
3880
+ if (pct >= 20) {
3881
+ recommendations.push({
3882
+ resource_id: resourceId,
3883
+ resource_name: current.resource_name,
3884
+ type: "switch_provider",
3885
+ description: `${cheapest.provider.toUpperCase()} offers this resource at $${cheapest.monthly_cost.toFixed(2)}/month vs $${current.monthly_cost.toFixed(2)}/month on ${current.provider.toUpperCase()}, saving approximately ${pct.toFixed(0)}%.`,
3886
+ current_monthly_cost: current.monthly_cost,
3887
+ estimated_monthly_cost: cheapest.monthly_cost,
3888
+ monthly_savings: savings,
3889
+ percentage_savings: Math.round(pct * 10) / 10,
3890
+ confidence: "medium",
3891
+ provider: cheapest.provider
3892
+ });
3893
+ }
3894
+ }
3895
+ }
3896
+ if (resource && OBJECT_STORAGE_RESOURCE_TYPES.has(resource.type)) {
3897
+ const storageClass = resource.attributes.storage_class ?? "";
3898
+ const isHotTier = storageClass === "" || storageClass.toLowerCase() === "standard" || storageClass.toLowerCase() === "hot";
3899
+ if (isHotTier && current.monthly_cost > 0) {
3900
+ const estimatedSavings = current.monthly_cost * 0.4;
3901
+ recommendations.push({
3902
+ resource_id: resourceId,
3903
+ resource_name: current.resource_name,
3904
+ type: "storage_tier",
3905
+ description: "Consider using an infrequent-access or cool storage tier if data is not accessed daily. This can reduce storage costs by ~40\u201360%.",
3906
+ current_monthly_cost: current.monthly_cost,
3907
+ estimated_monthly_cost: current.monthly_cost - estimatedSavings,
3908
+ monthly_savings: estimatedSavings,
3909
+ percentage_savings: 40,
3910
+ confidence: "low",
3911
+ provider: current.provider
3912
+ });
3913
+ }
3914
+ }
3915
+ }
3916
+ return recommendations;
3917
+ }
3918
+
3919
+ // src/reporting/markdown-report.ts
3920
+ function formatUsd(amount) {
3921
+ return `$${amount.toFixed(2)}`;
3922
+ }
3923
+ function providerLabel(provider) {
3924
+ switch (provider) {
3925
+ case "aws":
3926
+ return "AWS";
3927
+ case "azure":
3928
+ return "Azure";
3929
+ case "gcp":
3930
+ return "GCP";
3931
+ }
3932
+ }
3933
+ function generateMarkdownReport(comparison, resources, options = {}, parseWarnings = []) {
3934
+ const includeBreakdown = options.include_breakdown !== false;
3935
+ const includeRecommendations = options.include_recommendations !== false;
3936
+ const monthlyHours = options.monthly_hours ?? 730;
3937
+ const lines = [];
3938
+ const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3939
+ lines.push(
3940
+ `# Cloud Cost Comparison Report`,
3941
+ ``,
3942
+ `**Generated:** ${now} `,
3943
+ `**Source provider:** ${providerLabel(comparison.source_provider)} `,
3944
+ `**Resources analysed:** ${resources.length}`,
3945
+ ``
3946
+ );
3947
+ lines.push(`## Summary`, ``);
3948
+ const sourceBreakdown = comparison.comparisons.find(
3949
+ (b) => b.provider === comparison.source_provider
3950
+ );
3951
+ const sourceMonthly = sourceBreakdown?.total_monthly ?? 0;
3952
+ lines.push(
3953
+ `| Provider | Monthly Cost | Yearly Cost | vs Source |`,
3954
+ `|----------|-------------|------------|-----------|`
3955
+ );
3956
+ for (const breakdown of comparison.comparisons) {
3957
+ const diff = breakdown.total_monthly - sourceMonthly;
3958
+ const diffLabel = breakdown.provider === comparison.source_provider ? "\u2014" : diff < 0 ? `${formatUsd(Math.abs(diff))} cheaper` : diff > 0 ? `${formatUsd(diff)} more expensive` : "same cost";
3959
+ lines.push(
3960
+ `| ${providerLabel(breakdown.provider)} | ${formatUsd(breakdown.total_monthly)} | ${formatUsd(breakdown.total_yearly)} | ${diffLabel} |`
3961
+ );
3962
+ }
3963
+ lines.push(``);
3964
+ if (comparison.savings_summary.length > 0) {
3965
+ lines.push(`## Savings Potential`, ``);
3966
+ lines.push(
3967
+ `| Provider | Monthly Cost | Savings from Source | Savings % |`,
3968
+ `|----------|-------------|--------------------|-----------| `
3969
+ );
3970
+ for (const s of comparison.savings_summary) {
3971
+ const savings = s.difference_from_source;
3972
+ const savingsLabel = savings < 0 ? formatUsd(Math.abs(savings)) : `-${formatUsd(savings)}`;
3973
+ const pct = s.percentage_difference < 0 ? `${Math.abs(s.percentage_difference).toFixed(1)}% cheaper` : `${s.percentage_difference.toFixed(1)}% more`;
3974
+ lines.push(
3975
+ `| ${providerLabel(s.provider)} | ${formatUsd(s.total_monthly)} | ${savingsLabel} | ${pct} |`
3976
+ );
3977
+ }
3978
+ lines.push(``);
3979
+ }
3980
+ if (includeBreakdown && resources.length > 0) {
3981
+ lines.push(`## Resource Breakdown`, ``);
3982
+ const costMatrix = /* @__PURE__ */ new Map();
3983
+ for (const breakdown of comparison.comparisons) {
3984
+ for (const estimate of breakdown.by_resource) {
3985
+ const row = costMatrix.get(estimate.resource_id) ?? {};
3986
+ row[breakdown.provider] = estimate.monthly_cost;
3987
+ costMatrix.set(estimate.resource_id, row);
3988
+ }
3989
+ }
3990
+ const presentProviders = comparison.comparisons.map((b) => b.provider);
3991
+ const headerCols = presentProviders.map((p) => providerLabel(p)).join(" | ");
3992
+ lines.push(
3993
+ `| Resource | Type | ${headerCols} |`,
3994
+ `|----------|------|${presentProviders.map(() => "------").join("|")}|`
3995
+ );
3996
+ for (const resource of resources) {
3997
+ const costs = costMatrix.get(resource.id) ?? {};
3998
+ const costCols = presentProviders.map((p) => formatUsd(costs[p] ?? 0)).join(" | ");
3999
+ lines.push(
4000
+ `| ${resource.name} | ${resource.type} | ${costCols} |`
4001
+ );
4002
+ }
4003
+ lines.push(``);
4004
+ }
4005
+ if (includeRecommendations) {
4006
+ const allEstimates = comparison.comparisons.flatMap((b) => b.by_resource);
4007
+ const recommendations = generateOptimizations(allEstimates, resources);
4008
+ if (recommendations.length > 0) {
4009
+ lines.push(`## Savings Recommendations`, ``);
4010
+ for (const rec of recommendations) {
4011
+ const savingsLabel = `${formatUsd(rec.monthly_savings)}/month (${rec.percentage_savings}%)`;
4012
+ lines.push(
4013
+ `### ${rec.resource_name} \u2013 ${rec.type.replace(/_/g, " ")}`,
4014
+ ``,
4015
+ `${rec.description}`,
4016
+ ``,
4017
+ `- **Current cost:** ${formatUsd(rec.current_monthly_cost)}/month`,
4018
+ `- **Estimated after:** ${formatUsd(rec.estimated_monthly_cost)}/month`,
4019
+ `- **Potential savings:** ${savingsLabel}`,
4020
+ `- **Confidence:** ${rec.confidence}`,
4021
+ ``
4022
+ );
4023
+ }
4024
+ } else {
4025
+ lines.push(`## Savings Recommendations`, ``, `No recommendations at this time.`, ``);
4026
+ }
4027
+ }
4028
+ lines.push(`## Assumptions`, ``);
4029
+ lines.push(
4030
+ `| Parameter | Value |`,
4031
+ `|-----------|-------|`,
4032
+ `| Pricing model | On-demand (no reserved or spot instances) |`,
4033
+ `| Operating system | Linux (unless specified in resource attributes) |`,
4034
+ `| Monthly hours | ${monthlyHours} |`,
4035
+ `| Currency | USD |`,
4036
+ `| Pricing source | Mix of live API and fallback/bundled data |`,
4037
+ `| Data transfer costs | Not included |`,
4038
+ `| Tax | Not included |`,
4039
+ ``
4040
+ );
4041
+ const allDataWarnings = comparison.comparisons.flatMap((b) => b.warnings ?? []);
4042
+ const allWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allDataWarnings])];
4043
+ if (allWarnings.length > 0) {
4044
+ lines.push(`## Limitations`, ``);
4045
+ lines.push(
4046
+ `> The following issues were detected during analysis that may affect estimate accuracy:`,
4047
+ ``
4048
+ );
4049
+ for (const w of allWarnings) {
4050
+ lines.push(`- ${w}`);
4051
+ }
4052
+ lines.push(``);
4053
+ }
4054
+ return lines.join("\n");
4055
+ }
4056
+
4057
+ // src/reporting/json-report.ts
4058
+ function generateJsonReport(comparison, resources, monthlyHours = 730, parseWarnings = []) {
4059
+ const sourcesSet = /* @__PURE__ */ new Set();
4060
+ for (const breakdown of comparison.comparisons) {
4061
+ for (const estimate of breakdown.by_resource) {
4062
+ const src = estimate.pricing_source;
4063
+ if (src) {
4064
+ sourcesSet.add(`${breakdown.provider}_${src}`);
4065
+ }
4066
+ }
4067
+ }
4068
+ const allDataWarnings = comparison.comparisons.flatMap((b) => b.warnings ?? []);
4069
+ const allWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allDataWarnings])];
4070
+ const output = {
4071
+ metadata: {
4072
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
4073
+ pricing_sources_used: Array.from(sourcesSet).sort(),
4074
+ assumptions: {
4075
+ pricing_model: "on-demand",
4076
+ operating_system: "Linux (unless specified)",
4077
+ monthly_hours: monthlyHours,
4078
+ currency: "USD",
4079
+ data_transfer_costs: "not included",
4080
+ tax: "not included"
4081
+ },
4082
+ warnings: allWarnings
4083
+ },
4084
+ ...comparison,
4085
+ resource_summary: {
4086
+ total: resources.length,
4087
+ by_type: resources.reduce((acc, r) => {
4088
+ acc[r.type] = (acc[r.type] ?? 0) + 1;
4089
+ return acc;
4090
+ }, {})
4091
+ }
4092
+ };
4093
+ return JSON.stringify(output, null, 2);
4094
+ }
4095
+
4096
+ // src/reporting/csv-report.ts
4097
+ var PROVIDERS = ["aws", "azure", "gcp"];
4098
+ function csvEscape(value) {
4099
+ const str2 = String(value);
4100
+ if (str2.includes(",") || str2.includes("\n") || str2.includes('"')) {
4101
+ return `"${str2.replace(/"/g, '""')}"`;
4102
+ }
4103
+ return str2;
4104
+ }
4105
+ function generateCsvReport(comparison, resources) {
4106
+ const lines = [];
4107
+ lines.push("# Prices are estimates based on on-demand pricing. See assumptions in JSON/Markdown reports.");
4108
+ lines.push("Resource,Type,AWS Monthly,Azure Monthly,GCP Monthly,Cheapest");
4109
+ const costByProviderAndResource = /* @__PURE__ */ new Map();
4110
+ for (const breakdown of comparison.comparisons) {
4111
+ const byResource = /* @__PURE__ */ new Map();
4112
+ for (const estimate of breakdown.by_resource) {
4113
+ byResource.set(estimate.resource_id, estimate.monthly_cost);
4114
+ }
4115
+ costByProviderAndResource.set(breakdown.provider, byResource);
4116
+ }
4117
+ for (const resource of resources) {
4118
+ const costs = {};
4119
+ for (const provider of PROVIDERS) {
4120
+ const providerMap = costByProviderAndResource.get(provider);
4121
+ if (providerMap) {
4122
+ costs[provider] = providerMap.get(resource.id) ?? 0;
4123
+ }
4124
+ }
4125
+ let cheapest = "N/A";
4126
+ let lowestCost = Infinity;
4127
+ for (const provider of PROVIDERS) {
4128
+ const cost = costs[provider] ?? 0;
4129
+ if (cost > 0 && cost < lowestCost) {
4130
+ lowestCost = cost;
4131
+ cheapest = provider.toUpperCase();
4132
+ }
4133
+ }
4134
+ const row = [
4135
+ csvEscape(resource.name),
4136
+ csvEscape(resource.type),
4137
+ csvEscape((costs.aws ?? 0).toFixed(2)),
4138
+ csvEscape((costs.azure ?? 0).toFixed(2)),
4139
+ csvEscape((costs.gcp ?? 0).toFixed(2)),
4140
+ csvEscape(cheapest)
4141
+ ];
4142
+ lines.push(row.join(","));
4143
+ }
4144
+ return lines.join("\n");
4145
+ }
4146
+
4147
+ // src/tools/compare-providers.ts
4148
+ var compareProvidersSchema = z3.object({
4149
+ files: z3.array(
4150
+ z3.object({
4151
+ path: z3.string().describe("File path"),
4152
+ content: z3.string().describe("File content (HCL)")
4153
+ })
4154
+ ),
4155
+ tfvars: z3.string().optional().describe("Contents of terraform.tfvars file"),
4156
+ format: z3.enum(["markdown", "json", "csv"]).default("markdown").describe("Output report format"),
4157
+ providers: z3.array(z3.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison")
4158
+ });
4159
+ async function compareProviders(params, pricingEngine, config) {
4160
+ const inventory = await parseTerraform(params.files, params.tfvars);
4161
+ const sourceProvider = inventory.provider;
4162
+ const costEngine = new CostEngine(pricingEngine, config);
4163
+ const comparisons = [];
4164
+ for (const provider of params.providers) {
4165
+ const targetRegion = mapRegion(inventory.region, sourceProvider, provider);
4166
+ const breakdown = await costEngine.calculateBreakdown(
4167
+ inventory.resources,
4168
+ provider,
4169
+ targetRegion
4170
+ );
4171
+ comparisons.push(breakdown);
4172
+ }
4173
+ const sourceBreakdown = comparisons.find((b) => b.provider === sourceProvider) ?? comparisons[0];
4174
+ const sourceMonthly = sourceBreakdown?.total_monthly ?? 0;
4175
+ const savingsSummary = comparisons.map((b) => {
4176
+ const diff = b.total_monthly - sourceMonthly;
4177
+ const pct = sourceMonthly !== 0 ? diff / sourceMonthly * 100 : 0;
4178
+ return {
4179
+ provider: b.provider,
4180
+ total_monthly: b.total_monthly,
4181
+ difference_from_source: Math.round(diff * 100) / 100,
4182
+ percentage_difference: Math.round(pct * 10) / 10
4183
+ };
4184
+ });
4185
+ const comparison = {
4186
+ source_provider: sourceProvider,
4187
+ comparisons,
4188
+ savings_summary: savingsSummary,
4189
+ generated_at: (/* @__PURE__ */ new Date()).toISOString()
4190
+ };
4191
+ const allWarnings = comparisons.flatMap((b) => b.warnings ?? []);
4192
+ const parseWarnings = inventory.parse_warnings ?? [];
4193
+ const monthlyHours = config.pricing.monthly_hours;
4194
+ let report;
4195
+ switch (params.format) {
4196
+ case "json":
4197
+ report = generateJsonReport(comparison, inventory.resources, monthlyHours, parseWarnings);
4198
+ break;
4199
+ case "csv":
4200
+ report = generateCsvReport(comparison, inventory.resources);
4201
+ break;
4202
+ case "markdown":
4203
+ default:
4204
+ report = generateMarkdownReport(
4205
+ comparison,
4206
+ inventory.resources,
4207
+ {},
4208
+ parseWarnings
4209
+ );
4210
+ break;
4211
+ }
4212
+ return {
4213
+ report,
4214
+ format: params.format,
4215
+ comparison,
4216
+ warnings: [.../* @__PURE__ */ new Set([...parseWarnings, ...allWarnings])]
4217
+ };
4218
+ }
4219
+
4220
+ // src/tools/get-equivalents.ts
4221
+ import { z as z4 } from "zod";
4222
+
4223
+ // src/mapping/resource-mapper.ts
4224
+ function findEquivalent(resourceType, sourceProvider, targetProvider) {
4225
+ if (sourceProvider === targetProvider) {
4226
+ return resourceType;
4227
+ }
4228
+ const equivalents = getResourceEquivalents();
4229
+ const row = equivalents.find(
4230
+ (entry) => entry[sourceProvider] === resourceType
4231
+ );
4232
+ if (!row) return null;
4233
+ return row[targetProvider] ?? null;
4234
+ }
4235
+ function findAllEquivalents(resourceType, sourceProvider) {
4236
+ const equivalents = getResourceEquivalents();
4237
+ const row = equivalents.find(
4238
+ (entry) => entry[sourceProvider] === resourceType
4239
+ );
4240
+ if (!row) return {};
4241
+ const result = {};
4242
+ const providers = ["aws", "azure", "gcp"];
4243
+ for (const provider of providers) {
4244
+ const value = row[provider];
4245
+ if (value) {
4246
+ result[provider] = value;
4247
+ }
4248
+ }
4249
+ return result;
4250
+ }
4251
+
4252
+ // src/tools/get-equivalents.ts
4253
+ var getEquivalentsSchema = z4.object({
4254
+ resource_type: z4.string().describe(
4255
+ "Terraform resource type to look up (e.g. aws_instance, google_compute_instance)"
4256
+ ),
4257
+ source_provider: z4.enum(["aws", "azure", "gcp"]).describe("Cloud provider the resource type belongs to"),
4258
+ target_provider: z4.enum(["aws", "azure", "gcp"]).optional().describe(
4259
+ "Specific target provider. When omitted, equivalents for all providers are returned."
4260
+ ),
4261
+ instance_type: z4.string().optional().describe(
4262
+ "Instance type / VM size to also map across providers (e.g. t3.large, Standard_D4s_v3)"
4263
+ )
4264
+ });
4265
+ async function getEquivalents(params) {
4266
+ const sourceProvider = params.source_provider;
4267
+ let resourceEquivalents;
4268
+ if (params.target_provider) {
4269
+ const targetProvider = params.target_provider;
4270
+ const equivalent = findEquivalent(
4271
+ params.resource_type,
4272
+ sourceProvider,
4273
+ targetProvider
4274
+ );
4275
+ resourceEquivalents = { [targetProvider]: equivalent };
4276
+ } else {
4277
+ resourceEquivalents = findAllEquivalents(params.resource_type, sourceProvider);
4278
+ }
4279
+ let instanceEquivalents;
4280
+ if (params.instance_type) {
4281
+ const providers = ["aws", "azure", "gcp"];
4282
+ const targets = params.target_provider ? [params.target_provider] : providers.filter((p) => p !== sourceProvider);
4283
+ instanceEquivalents = {};
4284
+ for (const target of targets) {
4285
+ instanceEquivalents[target] = mapInstance(
4286
+ params.instance_type,
4287
+ sourceProvider,
4288
+ target
4289
+ );
4290
+ }
4291
+ }
4292
+ return {
4293
+ resource_type: params.resource_type,
4294
+ source_provider: sourceProvider,
4295
+ resource_equivalents: resourceEquivalents,
4296
+ ...instanceEquivalents !== void 0 ? {
4297
+ instance_type: params.instance_type,
4298
+ instance_equivalents: instanceEquivalents
4299
+ } : {}
4300
+ };
4301
+ }
4302
+
4303
+ // src/tools/get-pricing.ts
4304
+ import { z as z5 } from "zod";
4305
+ var getPricingSchema = z5.object({
4306
+ provider: z5.enum(["aws", "azure", "gcp"]).describe("Cloud provider to look up pricing for"),
4307
+ service: z5.enum(["compute", "database", "storage", "network", "kubernetes"]).describe("Service category"),
4308
+ resource_type: z5.string().describe(
4309
+ "Instance type, storage type, or resource identifier (e.g. t3.large, gp3, Standard_D4s_v3)"
4310
+ ),
4311
+ region: z5.string().describe("Cloud region (e.g. us-east-1, eastus, us-central1)")
4312
+ });
4313
+ async function getPricing(params, pricingEngine) {
4314
+ const provider = params.provider;
4315
+ const serviceMap = {
4316
+ compute: "compute",
4317
+ database: "database",
4318
+ storage: "storage",
4319
+ network: "nat-gateway",
4320
+ kubernetes: "kubernetes"
4321
+ };
4322
+ const service = serviceMap[params.service] ?? params.service;
4323
+ const price = await pricingEngine.getPrice(
4324
+ provider,
4325
+ service,
4326
+ params.resource_type,
4327
+ params.region
4328
+ );
4329
+ return {
4330
+ provider,
4331
+ service: params.service,
4332
+ resource_type: params.resource_type,
4333
+ region: params.region,
4334
+ price
4335
+ };
4336
+ }
4337
+
4338
+ // src/tools/optimize-cost.ts
4339
+ import { z as z6 } from "zod";
4340
+ var optimizeCostSchema = z6.object({
4341
+ files: z6.array(
4342
+ z6.object({
4343
+ path: z6.string().describe("File path"),
4344
+ content: z6.string().describe("File content (HCL)")
4345
+ })
4346
+ ),
4347
+ tfvars: z6.string().optional().describe("Contents of terraform.tfvars file"),
4348
+ providers: z6.array(z6.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Providers to calculate costs against for cross-provider recommendations")
4349
+ });
4350
+ async function optimizeCost(params, pricingEngine, config) {
4351
+ const inventory = await parseTerraform(params.files, params.tfvars);
4352
+ const sourceProvider = inventory.provider;
4353
+ const costEngine = new CostEngine(pricingEngine, config);
4354
+ const allEstimates = [];
4355
+ for (const provider of params.providers) {
4356
+ const targetRegion = mapRegion(inventory.region, sourceProvider, provider);
4357
+ const breakdown = await costEngine.calculateBreakdown(
4358
+ inventory.resources,
4359
+ provider,
4360
+ targetRegion
4361
+ );
4362
+ allEstimates.push(...breakdown.by_resource);
4363
+ }
4364
+ const recommendations = generateOptimizations(allEstimates, inventory.resources);
4365
+ const sourceEstimates = allEstimates.filter(
4366
+ (e) => e.provider === sourceProvider && e.monthly_cost > 0
4367
+ );
4368
+ const reservedPricing = sourceEstimates.map((estimate) => ({
4369
+ resource_id: estimate.resource_id,
4370
+ resource_name: estimate.resource_name,
4371
+ resource_type: estimate.resource_type,
4372
+ provider: estimate.provider,
4373
+ ...calculateReservedPricing(estimate.monthly_cost, estimate.provider)
4374
+ }));
4375
+ const totalPotentialSavings = recommendations.reduce((sum, r) => sum + r.monthly_savings, 0);
4376
+ return {
4377
+ recommendations,
4378
+ reserved_pricing: reservedPricing,
4379
+ total_potential_savings: Math.round(totalPotentialSavings * 100) / 100,
4380
+ recommendation_count: recommendations.length,
4381
+ resource_count: inventory.resources.length
4382
+ };
4383
+ }
4384
+
4385
+ // src/tools/index.ts
4386
+ function registerTools(server, config) {
4387
+ const cache = new PricingCache(config.cache.db_path);
4388
+ const pricingEngine = new PricingEngine(cache, config);
4389
+ server.tool(
4390
+ "analyze_terraform",
4391
+ "Parse Terraform HCL files and extract a resource inventory with provider detection, variable resolution, and cost-relevant attribute extraction",
4392
+ analyzeTerraformSchema.shape,
4393
+ async (params) => {
4394
+ const result = await analyzeTerraform(params);
4395
+ return {
4396
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4397
+ };
4398
+ }
4399
+ );
4400
+ server.tool(
4401
+ "estimate_cost",
4402
+ "Estimate monthly and yearly cloud costs for Terraform resources on a specific provider. Returns a full cost breakdown by resource and service category.",
4403
+ estimateCostSchema.shape,
4404
+ async (params) => {
4405
+ const result = await estimateCost(params, pricingEngine, config);
4406
+ return {
4407
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4408
+ };
4409
+ }
4410
+ );
4411
+ server.tool(
4412
+ "compare_providers",
4413
+ "Run a full cost comparison across AWS, Azure, and GCP for a set of Terraform resources. Returns a formatted report and raw comparison data including savings potential.",
4414
+ compareProvidersSchema.shape,
4415
+ async (params) => {
4416
+ const result = await compareProviders(params, pricingEngine, config);
4417
+ return {
4418
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4419
+ };
4420
+ }
4421
+ );
4422
+ server.tool(
4423
+ "get_equivalents",
4424
+ "Look up the equivalent Terraform resource types across cloud providers. Optionally also maps an instance type / VM size to the nearest equivalent on target providers.",
4425
+ getEquivalentsSchema.shape,
4426
+ async (params) => {
4427
+ const result = await getEquivalents(params);
4428
+ return {
4429
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4430
+ };
4431
+ }
4432
+ );
4433
+ server.tool(
4434
+ "get_pricing",
4435
+ "Direct pricing lookup for a specific cloud provider, service, resource type, and region. Returns the normalised unit price with metadata.",
4436
+ getPricingSchema.shape,
4437
+ async (params) => {
4438
+ const result = await getPricing(params, pricingEngine);
4439
+ return {
4440
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4441
+ };
4442
+ }
4443
+ );
4444
+ server.tool(
4445
+ "optimize_cost",
4446
+ "Analyse Terraform resources and return cost optimisation recommendations including right-sizing suggestions, reserved pricing comparisons, and cross-provider savings opportunities.",
4447
+ optimizeCostSchema.shape,
4448
+ async (params) => {
4449
+ const result = await optimizeCost(params, pricingEngine, config);
4450
+ return {
4451
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4452
+ };
4453
+ }
4454
+ );
4455
+ }
4456
+
4457
+ // src/server.ts
4458
+ async function startServer() {
4459
+ const config = loadConfig();
4460
+ setLogLevel(config.logging.level);
4461
+ const server = new McpServer({
4462
+ name: "cloudcost-mcp",
4463
+ version: "0.1.0"
4464
+ });
4465
+ registerTools(server, config);
4466
+ const transport = new StdioServerTransport();
4467
+ logger.info("Starting CloudCost MCP server");
4468
+ await server.connect(transport);
4469
+ }
4470
+
4471
+ // src/index.ts
4472
+ process.on("uncaughtException", (err) => {
4473
+ process.stderr.write(`Uncaught exception: ${err.stack ?? err.message}
4474
+ `);
4475
+ process.exit(1);
4476
+ });
4477
+ process.on("unhandledRejection", (reason) => {
4478
+ const msg = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
4479
+ process.stderr.write(`Unhandled rejection: ${msg}
4480
+ `);
4481
+ process.exit(1);
4482
+ });
4483
+ startServer().catch((err) => {
4484
+ process.stderr.write(`Failed to start server: ${err.stack ?? err.message}
4485
+ `);
4486
+ process.exit(1);
4487
+ });
4488
+ //# sourceMappingURL=index.js.map