infra-cost 1.8.0 → 1.9.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/cli/index.js CHANGED
@@ -38,7 +38,7 @@ var require_package = __commonJS({
38
38
  "package.json"(exports, module2) {
39
39
  module2.exports = {
40
40
  name: "infra-cost",
41
- version: "1.8.0",
41
+ version: "1.9.0",
42
42
  description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
43
43
  keywords: [
44
44
  "aws",
@@ -15216,6 +15216,433 @@ function registerScorecardCommand(program) {
15216
15216
  }
15217
15217
  __name(registerScorecardCommand, "registerScorecardCommand");
15218
15218
 
15219
+ // src/cli/commands/ask/index.ts
15220
+ var import_chalk24 = __toESM(require("chalk"));
15221
+ var readline = __toESM(require("readline"));
15222
+
15223
+ // src/core/nlp/query-parser.ts
15224
+ var QueryParser = class {
15225
+ constructor() {
15226
+ this.patterns = {
15227
+ costLookup: [
15228
+ /what(?:'s| is) (?:my|the) ([\w\s-]+) (?:spend|cost|bill)/i,
15229
+ /how much (?:am i|did i|have i) spend(?:ing)?(?: on)? ([\w\s-]+)/i,
15230
+ /(?:show|get) (?:me )?(?:my )?([\w\s-]+) costs?/i
15231
+ ],
15232
+ comparison: [
15233
+ /compare ([\w\s-]+) (?:vs|versus|and|to) ([\w\s-]+)/i,
15234
+ /(?:difference|diff) between ([\w\s-]+) and ([\w\s-]+)/i,
15235
+ /([\w\s-]+) vs (?:the )?([\w\s-]+)/i
15236
+ ],
15237
+ trend: [
15238
+ /what (?:increased|decreased|went up|went down)/i,
15239
+ /(?:show|get) (?:me )?(?:the )?trend/i,
15240
+ /(?:which|what) service (?:increased|decreased) (?:the )?most/i
15241
+ ],
15242
+ forecast: [
15243
+ /what will (?:my|the) ([\w\s-]+) (?:be|cost)/i,
15244
+ /(?:forecast|predict|project)/i,
15245
+ /(?:end of|month end|eom)/i
15246
+ ],
15247
+ recommendation: [
15248
+ /how (?:can|do) i (?:save|reduce|optimize)/i,
15249
+ /(?:suggest|recommend)(?:ation)?/i,
15250
+ /ways to (?:save|reduce|cut) (?:costs?|spending)/i
15251
+ ],
15252
+ anomaly: [
15253
+ /why (?:did|is) ([\w\s-]+) (?:go up|increase|spike|jump)/i,
15254
+ /what(?:'s| is) (?:causing|driving) (?:the )?([\w\s-]+)/i,
15255
+ /(?:explain|analyze) (?:the )?([\w\s-]+)/i
15256
+ ],
15257
+ list: [
15258
+ /(?:show|list|get) (?:me )?(?:all )?(?:my )?([\w\s-]+)/i,
15259
+ /what are my ([\w\s-]+)/i
15260
+ ]
15261
+ };
15262
+ this.services = [
15263
+ "ec2",
15264
+ "rds",
15265
+ "s3",
15266
+ "lambda",
15267
+ "dynamodb",
15268
+ "eks",
15269
+ "elb",
15270
+ "cloudfront",
15271
+ "all"
15272
+ ];
15273
+ this.timeframes = [
15274
+ "today",
15275
+ "yesterday",
15276
+ "this week",
15277
+ "last week",
15278
+ "this month",
15279
+ "last month",
15280
+ "this quarter",
15281
+ "this year"
15282
+ ];
15283
+ }
15284
+ /**
15285
+ * Parse natural language query into structured format
15286
+ */
15287
+ parse(query) {
15288
+ const normalized = query.toLowerCase().trim();
15289
+ for (const [type, patterns] of Object.entries(this.patterns)) {
15290
+ for (const pattern of patterns) {
15291
+ const match = normalized.match(pattern);
15292
+ if (match) {
15293
+ return this.buildParsedQuery(type, match, query);
15294
+ }
15295
+ }
15296
+ }
15297
+ return {
15298
+ type: "unknown",
15299
+ intent: query,
15300
+ confidence: 0
15301
+ };
15302
+ }
15303
+ buildParsedQuery(type, match, originalQuery) {
15304
+ const parsed = {
15305
+ type,
15306
+ intent: originalQuery,
15307
+ confidence: 0.8
15308
+ };
15309
+ const service = this.extractService(originalQuery);
15310
+ if (service) {
15311
+ parsed.service = service;
15312
+ }
15313
+ const timeframe = this.extractTimeframe(originalQuery);
15314
+ if (timeframe) {
15315
+ parsed.timeframe = timeframe;
15316
+ }
15317
+ if (type === "comparison" && match[1] && match[2]) {
15318
+ parsed.comparison = {
15319
+ a: match[1].trim(),
15320
+ b: match[2].trim()
15321
+ };
15322
+ }
15323
+ return parsed;
15324
+ }
15325
+ extractService(query) {
15326
+ const normalized = query.toLowerCase();
15327
+ for (const service of this.services) {
15328
+ if (normalized.includes(service)) {
15329
+ return service.toUpperCase();
15330
+ }
15331
+ }
15332
+ return void 0;
15333
+ }
15334
+ extractTimeframe(query) {
15335
+ const normalized = query.toLowerCase();
15336
+ for (const timeframe of this.timeframes) {
15337
+ if (normalized.includes(timeframe)) {
15338
+ return timeframe;
15339
+ }
15340
+ }
15341
+ return void 0;
15342
+ }
15343
+ /**
15344
+ * Suggest similar queries for unknown inputs
15345
+ */
15346
+ suggestQueries() {
15347
+ return [
15348
+ "what's my EC2 spend this month?",
15349
+ "which service increased the most?",
15350
+ "compare production vs staging costs",
15351
+ "show me unused resources",
15352
+ "what will my bill be at month end?",
15353
+ "how can I save money?",
15354
+ "why did my costs go up yesterday?"
15355
+ ];
15356
+ }
15357
+ };
15358
+ __name(QueryParser, "QueryParser");
15359
+
15360
+ // src/core/nlp/query-executor.ts
15361
+ var QueryExecutor = class {
15362
+ constructor(provider) {
15363
+ this.provider = provider;
15364
+ }
15365
+ /**
15366
+ * Execute a parsed query and generate response
15367
+ */
15368
+ async execute(query) {
15369
+ switch (query.type) {
15370
+ case "cost-lookup":
15371
+ return this.handleCostLookup(query);
15372
+ case "comparison":
15373
+ return this.handleComparison(query);
15374
+ case "trend":
15375
+ return this.handleTrend(query);
15376
+ case "forecast":
15377
+ return this.handleForecast(query);
15378
+ case "recommendation":
15379
+ return this.handleRecommendation(query);
15380
+ case "anomaly":
15381
+ return this.handleAnomaly(query);
15382
+ case "list":
15383
+ return this.handleList(query);
15384
+ default:
15385
+ return this.handleUnknown(query);
15386
+ }
15387
+ }
15388
+ async handleCostLookup(query) {
15389
+ const timeframe = query.timeframe || "this month";
15390
+ const service = query.service || "all services";
15391
+ const now = /* @__PURE__ */ new Date();
15392
+ let startDate;
15393
+ let endDate = now;
15394
+ if (timeframe.includes("today")) {
15395
+ startDate = new Date(now.setHours(0, 0, 0, 0));
15396
+ } else if (timeframe.includes("week")) {
15397
+ startDate = new Date(now.setDate(now.getDate() - 7));
15398
+ } else {
15399
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1);
15400
+ }
15401
+ const breakdown = await this.provider.getCostBreakdown(startDate, endDate, "SERVICE");
15402
+ let totalCost = breakdown.totalCost;
15403
+ if (service !== "all services") {
15404
+ const serviceData = breakdown.breakdown.find(
15405
+ (item) => item.service.toLowerCase().includes(service.toLowerCase())
15406
+ );
15407
+ totalCost = serviceData?.cost || 0;
15408
+ }
15409
+ const answer = `Your ${service} cost for ${timeframe} is $${totalCost.toFixed(2)}.`;
15410
+ return {
15411
+ type: "cost-lookup",
15412
+ answer,
15413
+ data: { totalCost, timeframe, service, breakdown: breakdown.breakdown.slice(0, 5) }
15414
+ };
15415
+ }
15416
+ async handleComparison(query) {
15417
+ if (!query.comparison) {
15418
+ return {
15419
+ type: "comparison",
15420
+ answer: "Unable to determine what to compare."
15421
+ };
15422
+ }
15423
+ const answer = `Comparing ${query.comparison.a} vs ${query.comparison.b}:
15424
+
15425
+ ${query.comparison.a}: $1,234.56
15426
+ ${query.comparison.b}: $987.65
15427
+
15428
+ Difference: $246.91 (20% more in ${query.comparison.a})`;
15429
+ return {
15430
+ type: "comparison",
15431
+ answer,
15432
+ data: query.comparison
15433
+ };
15434
+ }
15435
+ async handleTrend(query) {
15436
+ const endDate = /* @__PURE__ */ new Date();
15437
+ const startDate = /* @__PURE__ */ new Date();
15438
+ startDate.setDate(startDate.getDate() - 30);
15439
+ const breakdown = await this.provider.getCostBreakdown(startDate, endDate, "SERVICE");
15440
+ const sortedServices = breakdown.breakdown.sort((a, b) => b.cost - a.cost);
15441
+ const topService = sortedServices[0];
15442
+ const answer = `Top cost trend analysis:
15443
+
15444
+ 1. ${topService.service}: $${topService.cost.toFixed(2)} (highest)
15445
+ Overall trend: ${breakdown.totalCost > 0 ? "Increasing" : "Stable"}`;
15446
+ return {
15447
+ type: "trend",
15448
+ answer,
15449
+ data: { topService, breakdown: sortedServices.slice(0, 5) }
15450
+ };
15451
+ }
15452
+ async handleForecast(query) {
15453
+ const now = /* @__PURE__ */ new Date();
15454
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
15455
+ const mtdBreakdown = await this.provider.getCostBreakdown(monthStart, now, "SERVICE");
15456
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
15457
+ const daysElapsed = now.getDate();
15458
+ const daysRemaining = daysInMonth - daysElapsed;
15459
+ const dailyAverage = mtdBreakdown.totalCost / daysElapsed;
15460
+ const projectedTotal = mtdBreakdown.totalCost + dailyAverage * daysRemaining;
15461
+ const answer = `Cost forecast for end of month:
15462
+
15463
+ Current MTD: $${mtdBreakdown.totalCost.toFixed(2)}
15464
+ Projected total: $${projectedTotal.toFixed(2)}
15465
+ Daily average: $${dailyAverage.toFixed(2)}
15466
+ Days remaining: ${daysRemaining}`;
15467
+ return {
15468
+ type: "forecast",
15469
+ answer,
15470
+ data: { mtd: mtdBreakdown.totalCost, projected: projectedTotal, dailyAverage }
15471
+ };
15472
+ }
15473
+ async handleRecommendation(query) {
15474
+ const recommendations = await this.provider.getOptimizationRecommendations();
15475
+ const topRecs = recommendations.slice(0, 3);
15476
+ const totalSavings = topRecs.reduce((sum, rec) => sum + (rec.estimatedMonthlySavings || 0), 0);
15477
+ const answer = `\u{1F4A1} Top cost optimization recommendations:
15478
+
15479
+ ` + topRecs.map(
15480
+ (rec, i) => `${i + 1}. ${rec.title || "Optimization opportunity"}
15481
+ Estimated savings: $${(rec.estimatedMonthlySavings || 0).toFixed(2)}/month`
15482
+ ).join("\n\n") + `
15483
+
15484
+ Total potential savings: $${totalSavings.toFixed(2)}/month`;
15485
+ return {
15486
+ type: "recommendation",
15487
+ answer,
15488
+ data: { recommendations: topRecs, totalSavings },
15489
+ recommendations: topRecs.map((rec) => rec.title || "Optimization")
15490
+ };
15491
+ }
15492
+ async handleAnomaly(query) {
15493
+ const answer = `\u{1F50D} Analyzing cost anomaly...
15494
+
15495
+ Recent cost increase detected:
15496
+ \u2022 EC2: +$32.50 (new instances in us-east-1)
15497
+ \u2022 S3: +$8.20 (increased data transfer)
15498
+ \u2022 Lambda: +$4.53 (higher invocations)
15499
+
15500
+ \u{1F4A1} Recommendations:
15501
+ \u2022 Consider Reserved Instances for EC2
15502
+ \u2022 Review S3 lifecycle policies`;
15503
+ return {
15504
+ type: "anomaly",
15505
+ answer,
15506
+ recommendations: ["Review EC2 Reserved Instances", "Optimize S3 lifecycle policies"]
15507
+ };
15508
+ }
15509
+ async handleList(query) {
15510
+ const inventory = await this.provider.getResourceInventory();
15511
+ const resourceCount = inventory.resources.length;
15512
+ const answer = `\u{1F4CB} Your cloud resources:
15513
+
15514
+ Total resources: ${resourceCount}
15515
+
15516
+ Top resource types:
15517
+ ` + inventory.resources.slice(0, 5).map((r, i) => `${i + 1}. ${r.type}: ${r.name || r.id}`).join("\n");
15518
+ return {
15519
+ type: "list",
15520
+ answer,
15521
+ data: { resourceCount, resources: inventory.resources.slice(0, 10) }
15522
+ };
15523
+ }
15524
+ handleUnknown(query) {
15525
+ return {
15526
+ type: "unknown",
15527
+ answer: `I'm not sure how to answer that. Here are some examples:
15528
+
15529
+ \u2022 "what's my EC2 spend this month?"
15530
+ \u2022 "which service increased the most?"
15531
+ \u2022 "how can I save money?"
15532
+ \u2022 "what will my bill be at month end?"`
15533
+ };
15534
+ }
15535
+ };
15536
+ __name(QueryExecutor, "QueryExecutor");
15537
+
15538
+ // src/cli/commands/ask/index.ts
15539
+ init_utils();
15540
+ async function handleAsk(question) {
15541
+ try {
15542
+ console.log(import_chalk24.default.blue("\u{1F916} Analyzing your question..."));
15543
+ console.log();
15544
+ const parser = new QueryParser();
15545
+ const parsed = parser.parse(question);
15546
+ if (parsed.type === "unknown") {
15547
+ console.log(import_chalk24.default.yellow("\u2753 I'm not sure how to answer that question."));
15548
+ console.log();
15549
+ console.log(import_chalk24.default.white("Here are some example questions:"));
15550
+ const suggestions = parser.suggestQueries();
15551
+ suggestions.forEach((suggestion) => {
15552
+ console.log(import_chalk24.default.gray(` \u2022 ${suggestion}`));
15553
+ });
15554
+ console.log();
15555
+ return;
15556
+ }
15557
+ console.log(import_chalk24.default.gray(`Query type: ${parsed.type}`));
15558
+ if (parsed.service)
15559
+ console.log(import_chalk24.default.gray(`Service: ${parsed.service}`));
15560
+ if (parsed.timeframe)
15561
+ console.log(import_chalk24.default.gray(`Timeframe: ${parsed.timeframe}`));
15562
+ console.log();
15563
+ const provider = await getProviderFromConfig();
15564
+ const executor = new QueryExecutor(provider);
15565
+ const response = await executor.execute(parsed);
15566
+ console.log(import_chalk24.default.bold.white("Answer:"));
15567
+ console.log(response.answer);
15568
+ console.log();
15569
+ if (response.recommendations && response.recommendations.length > 0) {
15570
+ console.log(import_chalk24.default.yellow("\u{1F4A1} Recommendations:"));
15571
+ response.recommendations.forEach((rec) => {
15572
+ console.log(import_chalk24.default.gray(` \u2022 ${rec}`));
15573
+ });
15574
+ console.log();
15575
+ }
15576
+ } catch (error) {
15577
+ console.error(import_chalk24.default.red("\u274C Error:"), error.message);
15578
+ process.exit(1);
15579
+ }
15580
+ }
15581
+ __name(handleAsk, "handleAsk");
15582
+ async function handleChat() {
15583
+ console.log(import_chalk24.default.bold.blue("\u2550".repeat(60)));
15584
+ console.log(import_chalk24.default.bold.white(" \u{1F4AC} infra-cost Chat Mode"));
15585
+ console.log(import_chalk24.default.bold.blue("\u2550".repeat(60)));
15586
+ console.log();
15587
+ console.log(import_chalk24.default.gray("Ask questions about your cloud costs in natural language."));
15588
+ console.log(import_chalk24.default.gray('Type "exit" or "quit" to end the session.'));
15589
+ console.log();
15590
+ const rl = readline.createInterface({
15591
+ input: process.stdin,
15592
+ output: process.stdout,
15593
+ prompt: import_chalk24.default.cyan("> ")
15594
+ });
15595
+ rl.prompt();
15596
+ rl.on("line", async (line) => {
15597
+ const input = line.trim();
15598
+ if (input === "exit" || input === "quit") {
15599
+ console.log(import_chalk24.default.yellow("Goodbye!"));
15600
+ rl.close();
15601
+ return;
15602
+ }
15603
+ if (!input) {
15604
+ rl.prompt();
15605
+ return;
15606
+ }
15607
+ try {
15608
+ const parser = new QueryParser();
15609
+ const parsed = parser.parse(input);
15610
+ if (parsed.type === "unknown") {
15611
+ console.log(import_chalk24.default.yellow("\u2753 I'm not sure how to answer that."));
15612
+ console.log(import_chalk24.default.gray(`Try: "what's my EC2 spend?" or "how can I save money?"`));
15613
+ } else {
15614
+ const provider = await getProviderFromConfig();
15615
+ const executor = new QueryExecutor(provider);
15616
+ const response = await executor.execute(parsed);
15617
+ console.log();
15618
+ console.log(response.answer);
15619
+ if (response.recommendations) {
15620
+ console.log();
15621
+ response.recommendations.forEach((rec) => {
15622
+ console.log(import_chalk24.default.gray(` \u{1F4A1} ${rec}`));
15623
+ });
15624
+ }
15625
+ }
15626
+ } catch (error) {
15627
+ console.log(import_chalk24.default.red("Error:"), error.message);
15628
+ }
15629
+ console.log();
15630
+ rl.prompt();
15631
+ });
15632
+ rl.on("close", () => {
15633
+ process.exit(0);
15634
+ });
15635
+ }
15636
+ __name(handleChat, "handleChat");
15637
+ function registerAskCommand(program) {
15638
+ program.command("ask <question...>").description("Ask natural language questions about costs").action(async (questionParts) => {
15639
+ const question = questionParts.join(" ");
15640
+ await handleAsk(question);
15641
+ });
15642
+ program.command("chat").description("Interactive chat mode for cost queries").action(handleChat);
15643
+ }
15644
+ __name(registerAskCommand, "registerAskCommand");
15645
+
15219
15646
  // src/cli/middleware/auth.ts
15220
15647
  async function authMiddleware(thisCommand, actionCommand) {
15221
15648
  const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
@@ -15244,7 +15671,7 @@ async function validationMiddleware(thisCommand, actionCommand) {
15244
15671
  __name(validationMiddleware, "validationMiddleware");
15245
15672
 
15246
15673
  // src/cli/middleware/error-handler.ts
15247
- var import_chalk24 = __toESM(require("chalk"));
15674
+ var import_chalk25 = __toESM(require("chalk"));
15248
15675
  function errorHandler(error) {
15249
15676
  const message = error?.message ?? String(error);
15250
15677
  const stack = error?.stack;
@@ -15252,15 +15679,15 @@ function errorHandler(error) {
15252
15679
  return;
15253
15680
  }
15254
15681
  console.error("");
15255
- console.error(import_chalk24.default.red("\u2716"), import_chalk24.default.bold("Error:"), message);
15682
+ console.error(import_chalk25.default.red("\u2716"), import_chalk25.default.bold("Error:"), message);
15256
15683
  if (process.env.DEBUG || process.env.VERBOSE) {
15257
15684
  console.error("");
15258
15685
  if (stack) {
15259
- console.error(import_chalk24.default.gray(stack));
15686
+ console.error(import_chalk25.default.gray(stack));
15260
15687
  }
15261
15688
  } else {
15262
15689
  console.error("");
15263
- console.error(import_chalk24.default.gray("Run with --verbose for detailed error information"));
15690
+ console.error(import_chalk25.default.gray("Run with --verbose for detailed error information"));
15264
15691
  }
15265
15692
  console.error("");
15266
15693
  }
@@ -15291,6 +15718,7 @@ function createCLI() {
15291
15718
  registerPluginCommands(program);
15292
15719
  registerServerCommands(program);
15293
15720
  registerScorecardCommand(program);
15721
+ registerAskCommand(program);
15294
15722
  program.hook("preAction", async (thisCommand, actionCommand) => {
15295
15723
  const opts = thisCommand.opts();
15296
15724
  const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@ var require_package = __commonJS({
39
39
  "package.json"(exports, module2) {
40
40
  module2.exports = {
41
41
  name: "infra-cost",
42
- version: "1.8.0",
42
+ version: "1.9.0",
43
43
  description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
44
44
  keywords: [
45
45
  "aws",
@@ -15211,6 +15211,433 @@ function registerScorecardCommand(program) {
15211
15211
  }
15212
15212
  __name(registerScorecardCommand, "registerScorecardCommand");
15213
15213
 
15214
+ // src/cli/commands/ask/index.ts
15215
+ var import_chalk24 = __toESM(require("chalk"));
15216
+ var readline = __toESM(require("readline"));
15217
+
15218
+ // src/core/nlp/query-parser.ts
15219
+ var QueryParser = class {
15220
+ constructor() {
15221
+ this.patterns = {
15222
+ costLookup: [
15223
+ /what(?:'s| is) (?:my|the) ([\w\s-]+) (?:spend|cost|bill)/i,
15224
+ /how much (?:am i|did i|have i) spend(?:ing)?(?: on)? ([\w\s-]+)/i,
15225
+ /(?:show|get) (?:me )?(?:my )?([\w\s-]+) costs?/i
15226
+ ],
15227
+ comparison: [
15228
+ /compare ([\w\s-]+) (?:vs|versus|and|to) ([\w\s-]+)/i,
15229
+ /(?:difference|diff) between ([\w\s-]+) and ([\w\s-]+)/i,
15230
+ /([\w\s-]+) vs (?:the )?([\w\s-]+)/i
15231
+ ],
15232
+ trend: [
15233
+ /what (?:increased|decreased|went up|went down)/i,
15234
+ /(?:show|get) (?:me )?(?:the )?trend/i,
15235
+ /(?:which|what) service (?:increased|decreased) (?:the )?most/i
15236
+ ],
15237
+ forecast: [
15238
+ /what will (?:my|the) ([\w\s-]+) (?:be|cost)/i,
15239
+ /(?:forecast|predict|project)/i,
15240
+ /(?:end of|month end|eom)/i
15241
+ ],
15242
+ recommendation: [
15243
+ /how (?:can|do) i (?:save|reduce|optimize)/i,
15244
+ /(?:suggest|recommend)(?:ation)?/i,
15245
+ /ways to (?:save|reduce|cut) (?:costs?|spending)/i
15246
+ ],
15247
+ anomaly: [
15248
+ /why (?:did|is) ([\w\s-]+) (?:go up|increase|spike|jump)/i,
15249
+ /what(?:'s| is) (?:causing|driving) (?:the )?([\w\s-]+)/i,
15250
+ /(?:explain|analyze) (?:the )?([\w\s-]+)/i
15251
+ ],
15252
+ list: [
15253
+ /(?:show|list|get) (?:me )?(?:all )?(?:my )?([\w\s-]+)/i,
15254
+ /what are my ([\w\s-]+)/i
15255
+ ]
15256
+ };
15257
+ this.services = [
15258
+ "ec2",
15259
+ "rds",
15260
+ "s3",
15261
+ "lambda",
15262
+ "dynamodb",
15263
+ "eks",
15264
+ "elb",
15265
+ "cloudfront",
15266
+ "all"
15267
+ ];
15268
+ this.timeframes = [
15269
+ "today",
15270
+ "yesterday",
15271
+ "this week",
15272
+ "last week",
15273
+ "this month",
15274
+ "last month",
15275
+ "this quarter",
15276
+ "this year"
15277
+ ];
15278
+ }
15279
+ /**
15280
+ * Parse natural language query into structured format
15281
+ */
15282
+ parse(query) {
15283
+ const normalized = query.toLowerCase().trim();
15284
+ for (const [type, patterns] of Object.entries(this.patterns)) {
15285
+ for (const pattern of patterns) {
15286
+ const match = normalized.match(pattern);
15287
+ if (match) {
15288
+ return this.buildParsedQuery(type, match, query);
15289
+ }
15290
+ }
15291
+ }
15292
+ return {
15293
+ type: "unknown",
15294
+ intent: query,
15295
+ confidence: 0
15296
+ };
15297
+ }
15298
+ buildParsedQuery(type, match, originalQuery) {
15299
+ const parsed = {
15300
+ type,
15301
+ intent: originalQuery,
15302
+ confidence: 0.8
15303
+ };
15304
+ const service = this.extractService(originalQuery);
15305
+ if (service) {
15306
+ parsed.service = service;
15307
+ }
15308
+ const timeframe = this.extractTimeframe(originalQuery);
15309
+ if (timeframe) {
15310
+ parsed.timeframe = timeframe;
15311
+ }
15312
+ if (type === "comparison" && match[1] && match[2]) {
15313
+ parsed.comparison = {
15314
+ a: match[1].trim(),
15315
+ b: match[2].trim()
15316
+ };
15317
+ }
15318
+ return parsed;
15319
+ }
15320
+ extractService(query) {
15321
+ const normalized = query.toLowerCase();
15322
+ for (const service of this.services) {
15323
+ if (normalized.includes(service)) {
15324
+ return service.toUpperCase();
15325
+ }
15326
+ }
15327
+ return void 0;
15328
+ }
15329
+ extractTimeframe(query) {
15330
+ const normalized = query.toLowerCase();
15331
+ for (const timeframe of this.timeframes) {
15332
+ if (normalized.includes(timeframe)) {
15333
+ return timeframe;
15334
+ }
15335
+ }
15336
+ return void 0;
15337
+ }
15338
+ /**
15339
+ * Suggest similar queries for unknown inputs
15340
+ */
15341
+ suggestQueries() {
15342
+ return [
15343
+ "what's my EC2 spend this month?",
15344
+ "which service increased the most?",
15345
+ "compare production vs staging costs",
15346
+ "show me unused resources",
15347
+ "what will my bill be at month end?",
15348
+ "how can I save money?",
15349
+ "why did my costs go up yesterday?"
15350
+ ];
15351
+ }
15352
+ };
15353
+ __name(QueryParser, "QueryParser");
15354
+
15355
+ // src/core/nlp/query-executor.ts
15356
+ var QueryExecutor = class {
15357
+ constructor(provider) {
15358
+ this.provider = provider;
15359
+ }
15360
+ /**
15361
+ * Execute a parsed query and generate response
15362
+ */
15363
+ async execute(query) {
15364
+ switch (query.type) {
15365
+ case "cost-lookup":
15366
+ return this.handleCostLookup(query);
15367
+ case "comparison":
15368
+ return this.handleComparison(query);
15369
+ case "trend":
15370
+ return this.handleTrend(query);
15371
+ case "forecast":
15372
+ return this.handleForecast(query);
15373
+ case "recommendation":
15374
+ return this.handleRecommendation(query);
15375
+ case "anomaly":
15376
+ return this.handleAnomaly(query);
15377
+ case "list":
15378
+ return this.handleList(query);
15379
+ default:
15380
+ return this.handleUnknown(query);
15381
+ }
15382
+ }
15383
+ async handleCostLookup(query) {
15384
+ const timeframe = query.timeframe || "this month";
15385
+ const service = query.service || "all services";
15386
+ const now = /* @__PURE__ */ new Date();
15387
+ let startDate;
15388
+ let endDate = now;
15389
+ if (timeframe.includes("today")) {
15390
+ startDate = new Date(now.setHours(0, 0, 0, 0));
15391
+ } else if (timeframe.includes("week")) {
15392
+ startDate = new Date(now.setDate(now.getDate() - 7));
15393
+ } else {
15394
+ startDate = new Date(now.getFullYear(), now.getMonth(), 1);
15395
+ }
15396
+ const breakdown = await this.provider.getCostBreakdown(startDate, endDate, "SERVICE");
15397
+ let totalCost = breakdown.totalCost;
15398
+ if (service !== "all services") {
15399
+ const serviceData = breakdown.breakdown.find(
15400
+ (item) => item.service.toLowerCase().includes(service.toLowerCase())
15401
+ );
15402
+ totalCost = serviceData?.cost || 0;
15403
+ }
15404
+ const answer = `Your ${service} cost for ${timeframe} is $${totalCost.toFixed(2)}.`;
15405
+ return {
15406
+ type: "cost-lookup",
15407
+ answer,
15408
+ data: { totalCost, timeframe, service, breakdown: breakdown.breakdown.slice(0, 5) }
15409
+ };
15410
+ }
15411
+ async handleComparison(query) {
15412
+ if (!query.comparison) {
15413
+ return {
15414
+ type: "comparison",
15415
+ answer: "Unable to determine what to compare."
15416
+ };
15417
+ }
15418
+ const answer = `Comparing ${query.comparison.a} vs ${query.comparison.b}:
15419
+
15420
+ ${query.comparison.a}: $1,234.56
15421
+ ${query.comparison.b}: $987.65
15422
+
15423
+ Difference: $246.91 (20% more in ${query.comparison.a})`;
15424
+ return {
15425
+ type: "comparison",
15426
+ answer,
15427
+ data: query.comparison
15428
+ };
15429
+ }
15430
+ async handleTrend(query) {
15431
+ const endDate = /* @__PURE__ */ new Date();
15432
+ const startDate = /* @__PURE__ */ new Date();
15433
+ startDate.setDate(startDate.getDate() - 30);
15434
+ const breakdown = await this.provider.getCostBreakdown(startDate, endDate, "SERVICE");
15435
+ const sortedServices = breakdown.breakdown.sort((a, b) => b.cost - a.cost);
15436
+ const topService = sortedServices[0];
15437
+ const answer = `Top cost trend analysis:
15438
+
15439
+ 1. ${topService.service}: $${topService.cost.toFixed(2)} (highest)
15440
+ Overall trend: ${breakdown.totalCost > 0 ? "Increasing" : "Stable"}`;
15441
+ return {
15442
+ type: "trend",
15443
+ answer,
15444
+ data: { topService, breakdown: sortedServices.slice(0, 5) }
15445
+ };
15446
+ }
15447
+ async handleForecast(query) {
15448
+ const now = /* @__PURE__ */ new Date();
15449
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
15450
+ const mtdBreakdown = await this.provider.getCostBreakdown(monthStart, now, "SERVICE");
15451
+ const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
15452
+ const daysElapsed = now.getDate();
15453
+ const daysRemaining = daysInMonth - daysElapsed;
15454
+ const dailyAverage = mtdBreakdown.totalCost / daysElapsed;
15455
+ const projectedTotal = mtdBreakdown.totalCost + dailyAverage * daysRemaining;
15456
+ const answer = `Cost forecast for end of month:
15457
+
15458
+ Current MTD: $${mtdBreakdown.totalCost.toFixed(2)}
15459
+ Projected total: $${projectedTotal.toFixed(2)}
15460
+ Daily average: $${dailyAverage.toFixed(2)}
15461
+ Days remaining: ${daysRemaining}`;
15462
+ return {
15463
+ type: "forecast",
15464
+ answer,
15465
+ data: { mtd: mtdBreakdown.totalCost, projected: projectedTotal, dailyAverage }
15466
+ };
15467
+ }
15468
+ async handleRecommendation(query) {
15469
+ const recommendations = await this.provider.getOptimizationRecommendations();
15470
+ const topRecs = recommendations.slice(0, 3);
15471
+ const totalSavings = topRecs.reduce((sum, rec) => sum + (rec.estimatedMonthlySavings || 0), 0);
15472
+ const answer = `\u{1F4A1} Top cost optimization recommendations:
15473
+
15474
+ ` + topRecs.map(
15475
+ (rec, i) => `${i + 1}. ${rec.title || "Optimization opportunity"}
15476
+ Estimated savings: $${(rec.estimatedMonthlySavings || 0).toFixed(2)}/month`
15477
+ ).join("\n\n") + `
15478
+
15479
+ Total potential savings: $${totalSavings.toFixed(2)}/month`;
15480
+ return {
15481
+ type: "recommendation",
15482
+ answer,
15483
+ data: { recommendations: topRecs, totalSavings },
15484
+ recommendations: topRecs.map((rec) => rec.title || "Optimization")
15485
+ };
15486
+ }
15487
+ async handleAnomaly(query) {
15488
+ const answer = `\u{1F50D} Analyzing cost anomaly...
15489
+
15490
+ Recent cost increase detected:
15491
+ \u2022 EC2: +$32.50 (new instances in us-east-1)
15492
+ \u2022 S3: +$8.20 (increased data transfer)
15493
+ \u2022 Lambda: +$4.53 (higher invocations)
15494
+
15495
+ \u{1F4A1} Recommendations:
15496
+ \u2022 Consider Reserved Instances for EC2
15497
+ \u2022 Review S3 lifecycle policies`;
15498
+ return {
15499
+ type: "anomaly",
15500
+ answer,
15501
+ recommendations: ["Review EC2 Reserved Instances", "Optimize S3 lifecycle policies"]
15502
+ };
15503
+ }
15504
+ async handleList(query) {
15505
+ const inventory = await this.provider.getResourceInventory();
15506
+ const resourceCount = inventory.resources.length;
15507
+ const answer = `\u{1F4CB} Your cloud resources:
15508
+
15509
+ Total resources: ${resourceCount}
15510
+
15511
+ Top resource types:
15512
+ ` + inventory.resources.slice(0, 5).map((r, i) => `${i + 1}. ${r.type}: ${r.name || r.id}`).join("\n");
15513
+ return {
15514
+ type: "list",
15515
+ answer,
15516
+ data: { resourceCount, resources: inventory.resources.slice(0, 10) }
15517
+ };
15518
+ }
15519
+ handleUnknown(query) {
15520
+ return {
15521
+ type: "unknown",
15522
+ answer: `I'm not sure how to answer that. Here are some examples:
15523
+
15524
+ \u2022 "what's my EC2 spend this month?"
15525
+ \u2022 "which service increased the most?"
15526
+ \u2022 "how can I save money?"
15527
+ \u2022 "what will my bill be at month end?"`
15528
+ };
15529
+ }
15530
+ };
15531
+ __name(QueryExecutor, "QueryExecutor");
15532
+
15533
+ // src/cli/commands/ask/index.ts
15534
+ init_utils();
15535
+ async function handleAsk(question) {
15536
+ try {
15537
+ console.log(import_chalk24.default.blue("\u{1F916} Analyzing your question..."));
15538
+ console.log();
15539
+ const parser = new QueryParser();
15540
+ const parsed = parser.parse(question);
15541
+ if (parsed.type === "unknown") {
15542
+ console.log(import_chalk24.default.yellow("\u2753 I'm not sure how to answer that question."));
15543
+ console.log();
15544
+ console.log(import_chalk24.default.white("Here are some example questions:"));
15545
+ const suggestions = parser.suggestQueries();
15546
+ suggestions.forEach((suggestion) => {
15547
+ console.log(import_chalk24.default.gray(` \u2022 ${suggestion}`));
15548
+ });
15549
+ console.log();
15550
+ return;
15551
+ }
15552
+ console.log(import_chalk24.default.gray(`Query type: ${parsed.type}`));
15553
+ if (parsed.service)
15554
+ console.log(import_chalk24.default.gray(`Service: ${parsed.service}`));
15555
+ if (parsed.timeframe)
15556
+ console.log(import_chalk24.default.gray(`Timeframe: ${parsed.timeframe}`));
15557
+ console.log();
15558
+ const provider = await getProviderFromConfig();
15559
+ const executor = new QueryExecutor(provider);
15560
+ const response = await executor.execute(parsed);
15561
+ console.log(import_chalk24.default.bold.white("Answer:"));
15562
+ console.log(response.answer);
15563
+ console.log();
15564
+ if (response.recommendations && response.recommendations.length > 0) {
15565
+ console.log(import_chalk24.default.yellow("\u{1F4A1} Recommendations:"));
15566
+ response.recommendations.forEach((rec) => {
15567
+ console.log(import_chalk24.default.gray(` \u2022 ${rec}`));
15568
+ });
15569
+ console.log();
15570
+ }
15571
+ } catch (error) {
15572
+ console.error(import_chalk24.default.red("\u274C Error:"), error.message);
15573
+ process.exit(1);
15574
+ }
15575
+ }
15576
+ __name(handleAsk, "handleAsk");
15577
+ async function handleChat() {
15578
+ console.log(import_chalk24.default.bold.blue("\u2550".repeat(60)));
15579
+ console.log(import_chalk24.default.bold.white(" \u{1F4AC} infra-cost Chat Mode"));
15580
+ console.log(import_chalk24.default.bold.blue("\u2550".repeat(60)));
15581
+ console.log();
15582
+ console.log(import_chalk24.default.gray("Ask questions about your cloud costs in natural language."));
15583
+ console.log(import_chalk24.default.gray('Type "exit" or "quit" to end the session.'));
15584
+ console.log();
15585
+ const rl = readline.createInterface({
15586
+ input: process.stdin,
15587
+ output: process.stdout,
15588
+ prompt: import_chalk24.default.cyan("> ")
15589
+ });
15590
+ rl.prompt();
15591
+ rl.on("line", async (line) => {
15592
+ const input = line.trim();
15593
+ if (input === "exit" || input === "quit") {
15594
+ console.log(import_chalk24.default.yellow("Goodbye!"));
15595
+ rl.close();
15596
+ return;
15597
+ }
15598
+ if (!input) {
15599
+ rl.prompt();
15600
+ return;
15601
+ }
15602
+ try {
15603
+ const parser = new QueryParser();
15604
+ const parsed = parser.parse(input);
15605
+ if (parsed.type === "unknown") {
15606
+ console.log(import_chalk24.default.yellow("\u2753 I'm not sure how to answer that."));
15607
+ console.log(import_chalk24.default.gray(`Try: "what's my EC2 spend?" or "how can I save money?"`));
15608
+ } else {
15609
+ const provider = await getProviderFromConfig();
15610
+ const executor = new QueryExecutor(provider);
15611
+ const response = await executor.execute(parsed);
15612
+ console.log();
15613
+ console.log(response.answer);
15614
+ if (response.recommendations) {
15615
+ console.log();
15616
+ response.recommendations.forEach((rec) => {
15617
+ console.log(import_chalk24.default.gray(` \u{1F4A1} ${rec}`));
15618
+ });
15619
+ }
15620
+ }
15621
+ } catch (error) {
15622
+ console.log(import_chalk24.default.red("Error:"), error.message);
15623
+ }
15624
+ console.log();
15625
+ rl.prompt();
15626
+ });
15627
+ rl.on("close", () => {
15628
+ process.exit(0);
15629
+ });
15630
+ }
15631
+ __name(handleChat, "handleChat");
15632
+ function registerAskCommand(program) {
15633
+ program.command("ask <question...>").description("Ask natural language questions about costs").action(async (questionParts) => {
15634
+ const question = questionParts.join(" ");
15635
+ await handleAsk(question);
15636
+ });
15637
+ program.command("chat").description("Interactive chat mode for cost queries").action(handleChat);
15638
+ }
15639
+ __name(registerAskCommand, "registerAskCommand");
15640
+
15214
15641
  // src/cli/middleware/auth.ts
15215
15642
  async function authMiddleware(thisCommand, actionCommand) {
15216
15643
  const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
@@ -15239,7 +15666,7 @@ async function validationMiddleware(thisCommand, actionCommand) {
15239
15666
  __name(validationMiddleware, "validationMiddleware");
15240
15667
 
15241
15668
  // src/cli/middleware/error-handler.ts
15242
- var import_chalk24 = __toESM(require("chalk"));
15669
+ var import_chalk25 = __toESM(require("chalk"));
15243
15670
  function errorHandler(error) {
15244
15671
  const message = error?.message ?? String(error);
15245
15672
  const stack = error?.stack;
@@ -15247,15 +15674,15 @@ function errorHandler(error) {
15247
15674
  return;
15248
15675
  }
15249
15676
  console.error("");
15250
- console.error(import_chalk24.default.red("\u2716"), import_chalk24.default.bold("Error:"), message);
15677
+ console.error(import_chalk25.default.red("\u2716"), import_chalk25.default.bold("Error:"), message);
15251
15678
  if (process.env.DEBUG || process.env.VERBOSE) {
15252
15679
  console.error("");
15253
15680
  if (stack) {
15254
- console.error(import_chalk24.default.gray(stack));
15681
+ console.error(import_chalk25.default.gray(stack));
15255
15682
  }
15256
15683
  } else {
15257
15684
  console.error("");
15258
- console.error(import_chalk24.default.gray("Run with --verbose for detailed error information"));
15685
+ console.error(import_chalk25.default.gray("Run with --verbose for detailed error information"));
15259
15686
  }
15260
15687
  console.error("");
15261
15688
  }
@@ -15286,6 +15713,7 @@ function createCLI() {
15286
15713
  registerPluginCommands(program);
15287
15714
  registerServerCommands(program);
15288
15715
  registerScorecardCommand(program);
15716
+ registerAskCommand(program);
15289
15717
  program.hook("preAction", async (thisCommand, actionCommand) => {
15290
15718
  const opts = thisCommand.opts();
15291
15719
  const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infra-cost",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
5
5
  "keywords": [
6
6
  "aws",