@aaa-sentinel/mcp-server 0.7.0 → 0.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.
Files changed (3) hide show
  1. package/README.md +11 -9
  2. package/dist/cli.js +121 -7
  3. package/package.json +45 -45
package/README.md CHANGED
@@ -29,7 +29,7 @@ dashboard you have to remember to visit.
29
29
  only those; the scopes you leave out are simply not reachable from the assistant.
30
30
  - A **Pro** plan or above: the API is not available on the free plan. Chargeback, cost anomalies,
31
31
  unit cost, managed clients and the agent need **Business**, and the security tools also need the
32
- security add-on on the account — the table below says which.
32
+ risk and compliance add-on on the account — the table below says which.
33
33
 
34
34
  ## Configuration
35
35
 
@@ -107,12 +107,14 @@ status as plain JSON.
107
107
  | `get_dashboard` | `accountId?` | `dashboard:read` | Pro |
108
108
  | `get_cost_forecast` | `accountId` | `reports:read` | Pro |
109
109
  | `list_cost_anomalies` | `accountId?` | `reports:read` | Business |
110
- | `get_security_overview` | — | `security:read` | Pro + security add-on |
111
- | `get_security_score` | `accountId` | `security:read` | Pro + security add-on |
112
- | `list_security_findings` | `accountId`, `source?`, `category?` | `security:read` | Pro + security add-on |
113
- | `get_security_critical_changes` | `accountId` | `security:read` | Pro + security add-on |
114
- | `list_security_vulnerabilities` | `accountId` | `security:read` | Pro + security add-on |
115
- | `get_security_conformance` | `accountId` or `clientId` | `security:read` | Pro + security add-on |
110
+ | `get_security_overview` | — | `security:read` | Pro + risk and compliance add-on |
111
+ | `get_security_score` | `accountId` | `security:read` | Pro + risk and compliance add-on |
112
+ | `list_security_findings` | `accountId`, `source?`, `category?` | `security:read` | Pro + risk and compliance add-on |
113
+ | `get_security_critical_changes` | `accountId` | `security:read` | Pro + risk and compliance add-on |
114
+ | `list_security_vulnerabilities` | `accountId` | `security:read` | Pro + risk and compliance add-on |
115
+ | `get_security_tool_spend` | `accountId` | `security:read` | Pro + risk and compliance add-on |
116
+ | `get_security_conformance` | `accountId` or `clientId` | `security:read` | Pro + risk and compliance add-on |
117
+ | `get_security_posture_history` | `accountId` or `clientId`, `days?` | `security:read` | Pro + risk and compliance add-on |
116
118
  | `get_account` | `accountId` | `accounts:read` | Pro |
117
119
  | `get_account_monthly_costs` | `accountId` | `accounts:read` | Pro |
118
120
  | `get_cost_history` | `accountId` | `reports:read` | Pro |
@@ -164,14 +166,14 @@ not an outage yet), `down`, or `recovering` (still counted as down until the rec
164
166
  confirmed). The last result carries a reason code such as `timeout`, `status_mismatch` or
165
167
  `queue_dlq_not_empty`; the full list is in the API reference.
166
168
 
167
- The security tools need the security add-on enabled on the account (20 €/month per account).
169
+ The security tools need the risk and compliance add-on enabled on the account (20 €/month per account).
168
170
  Without it they answer with `access: "teaser"` and no scan data, so the assistant can tell
169
171
  "not enabled" apart from "nothing found".
170
172
 
171
173
  ### Why there is nothing that writes
172
174
 
173
175
  There is no tool to approve a remediation, launch a playbook, stop an instance, start a security
174
- scan or enable the (billed) security add-on, and that is a
176
+ scan or enable the (billed) risk and compliance add-on, and that is a
175
177
  decision rather than a gap. An MCP server with write access puts a model — with its own idea of
176
178
  what is convenient, and a prompt an attacker may have influenced — inside the path that changes
177
179
  your infrastructure. Execution stays in the product, where it is gated by approvals, dry runs and
package/dist/cli.js CHANGED
@@ -2800,6 +2800,28 @@ var $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
2800
2800
  return payload;
2801
2801
  };
2802
2802
  });
2803
+ var $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
2804
+ $ZodType.init(inst, def);
2805
+ const values = new Set(def.values);
2806
+ inst._zod.values = values;
2807
+ defineLazyInternal(inst, "pattern", (zod) => {
2808
+ const vals = zod.def.values;
2809
+ return new RegExp(vals.length ? `^(${vals.map((o) => typeof o === "string" ? escapeRegex(o) : o ? escapeRegex(o.toString()) : String(o)).join("|")})$` : "^[^\\s\\S]$");
2810
+ });
2811
+ inst._zod.parse = (payload, _ctx) => {
2812
+ const input = payload.value;
2813
+ if (values.has(input)) {
2814
+ return payload;
2815
+ }
2816
+ payload.issues.push({
2817
+ code: "invalid_value",
2818
+ values: def.values,
2819
+ input,
2820
+ inst
2821
+ });
2822
+ return payload;
2823
+ };
2824
+ });
2803
2825
  var $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => {
2804
2826
  $ZodType.init(inst, def);
2805
2827
  inst._zod.optin = "optional";
@@ -4739,6 +4761,46 @@ var enumProcessor = (schema, _ctx, json, _params) => {
4739
4761
  json.type = "string";
4740
4762
  json.enum = values;
4741
4763
  };
4764
+ var literalProcessor = (schema, ctx, json, params) => {
4765
+ const def = schema._zod.def;
4766
+ if (def.values.length === 0) {
4767
+ json.not = {};
4768
+ return;
4769
+ }
4770
+ const vals = [];
4771
+ for (const val of def.values) {
4772
+ if (val === void 0) {
4773
+ if (handleUnrepresentable(schema, ctx, json, params, "Literal `undefined` cannot be represented in JSON Schema"))
4774
+ return;
4775
+ } else if (typeof val === "bigint") {
4776
+ if (handleUnrepresentable(schema, ctx, json, params, "BigInt literals cannot be represented in JSON Schema"))
4777
+ return;
4778
+ vals.push(Number(val));
4779
+ } else {
4780
+ vals.push(val);
4781
+ }
4782
+ }
4783
+ if (vals.length === 0) {
4784
+ } else if (vals.length === 1) {
4785
+ const val = vals[0];
4786
+ json.type = val === null ? "null" : typeof val;
4787
+ if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
4788
+ json.enum = [val];
4789
+ } else {
4790
+ json.const = val;
4791
+ }
4792
+ } else {
4793
+ if (vals.every((v) => typeof v === "number"))
4794
+ json.type = "number";
4795
+ if (vals.every((v) => typeof v === "string"))
4796
+ json.type = "string";
4797
+ if (vals.every((v) => typeof v === "boolean"))
4798
+ json.type = "boolean";
4799
+ if (vals.every((v) => v === null))
4800
+ json.type = "null";
4801
+ json.enum = vals;
4802
+ }
4803
+ };
4742
4804
  var customProcessor = (schema, ctx, json, params) => {
4743
4805
  handleUnrepresentable(schema, ctx, json, params, "Custom types cannot be represented in JSON Schema");
4744
4806
  };
@@ -5668,6 +5730,27 @@ function _enum(values, params) {
5668
5730
  ...util_exports.normalizeParams(params)
5669
5731
  });
5670
5732
  }
5733
+ var ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => {
5734
+ $ZodLiteral.init(inst, def);
5735
+ ZodType.init(inst, def);
5736
+ inst._zod.processJSONSchema = (ctx, json, params) => literalProcessor(inst, ctx, json, params);
5737
+ inst.values = new Set(def.values);
5738
+ Object.defineProperty(inst, "value", {
5739
+ get() {
5740
+ if (def.values.length > 1) {
5741
+ throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
5742
+ }
5743
+ return def.values[0];
5744
+ }
5745
+ });
5746
+ });
5747
+ function literal(value, params) {
5748
+ return new ZodLiteral({
5749
+ type: "literal",
5750
+ values: Array.isArray(value) ? value : [value],
5751
+ ...util_exports.normalizeParams(params)
5752
+ });
5753
+ }
5671
5754
  var ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
5672
5755
  _ensureDefaultMemoizer();
5673
5756
  $ZodTransform.init(inst, def);
@@ -5982,7 +6065,7 @@ function defineTool(def) {
5982
6065
  }
5983
6066
  var SAVINGS_TYPES = ["compute", "storage", "network", "database", "serverless", "governance"];
5984
6067
  var SECURITY_SOURCES = ["own", "corroborated", "securityhub", "guardduty", "defender", "scc"];
5985
- var SECURITY_CATEGORIES = ["security_network", "security_exposure", "security_encryption", "security_iam", "security_logging", "security_secrets"];
6068
+ var SECURITY_CATEGORIES = ["security_network", "security_exposure", "security_encryption", "security_iam", "security_logging", "security_secrets", "security_patching"];
5986
6069
  var AGENT_STATUSES = ["proposed", "pending_approval", "pending_second_approval", "approved", "executing", "completed", "failed", "cancelled", "expired"];
5987
6070
  var BillingPeriod = string2().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Use YYYY-MM");
5988
6071
  var ACCOUNT_ID = { type: "string", description: "Account identifier, as returned by list_accounts." };
@@ -6121,14 +6204,14 @@ var TOOLS = [
6121
6204
  // queda en la API y la app, no en un agente ajeno.
6122
6205
  defineTool({
6123
6206
  name: "get_security_overview",
6124
- description: 'Get the security posture of every cloud account: Score (0-100), band, findings by severity and attack chains. Accounts without the security add-on come back as access "teaser" with no data. Requires the security:read scope and the Pro plan or above.',
6207
+ description: 'Get the security posture of every cloud account: Score (0-100), band, findings by severity and attack chains. Accounts without the risk and compliance add-on come back as access "teaser" with no data. Requires the security:read scope and the Pro plan or above.',
6125
6208
  inputSchema: { type: "object", properties: {} },
6126
6209
  argsSchema: object({}),
6127
6210
  handler: async (_args, config2) => apiGet("/v1/security/overview", void 0, config2)
6128
6211
  }),
6129
6212
  defineTool({
6130
6213
  name: "get_security_score",
6131
- description: "Get the security Score of the latest security scan of one account, with its coverage (which rules could not be evaluated and which permission was missing). Requires the security:read scope, the Pro plan and the security add-on on that account.",
6214
+ description: "Get the security Score of the latest security scan of one account, with its coverage (which rules could not be evaluated and which permission was missing). Requires the security:read scope, the Pro plan and the risk and compliance add-on on that account.",
6132
6215
  inputSchema: {
6133
6216
  type: "object",
6134
6217
  properties: { accountId: { type: "string", description: "Account identifier, as returned by list_accounts." } },
@@ -6139,7 +6222,7 @@ var TOOLS = [
6139
6222
  }),
6140
6223
  defineTool({
6141
6224
  name: "list_security_findings",
6142
- description: 'List the security findings of the latest security scan of one account (up to 500, most severe first), with attack chains and a diff against the previous scan. Use category "security_iam" for the permissions view: effective administrators, unused identities, keys and service accounts, granted permissions that are not used, and assignments to deleted identities. Requires the security:read scope, the Pro plan and the security add-on on that account.',
6225
+ description: 'List the security findings of the latest security scan of one account (up to 500, most severe first), with attack chains and a diff against the previous scan. Use category "security_iam" for the permissions view: effective administrators, unused identities, keys and service accounts, granted permissions that are not used, and assignments to deleted identities. Each finding may carry resourceCost, and the response exposedCost (what the account pays per month in resources with critical or high findings): both are estimates from the latest cost scan of at most 14 days (list price or what the resource was billed), in USD and approximate, absent when there is no cost data. Each finding may also carry attribution (the Owner, Team and CostCenter tags of its resource; unowned when its tags were read and it has neither Owner nor Team; absent when its tags were not read), and the response attributionSummary (findings and exposed cost by team and by cost center); critical findings without an owner come first among the critical ones. Requires the security:read scope, the Pro plan and the risk and compliance add-on on that account.',
6143
6226
  inputSchema: {
6144
6227
  type: "object",
6145
6228
  properties: {
@@ -6166,7 +6249,7 @@ var TOOLS = [
6166
6249
  }),
6167
6250
  defineTool({
6168
6251
  name: "get_security_critical_changes",
6169
- description: "List the critical configuration changes of the last 7 days in one account (up to 100, newest first), read from CloudTrail in AWS, the activity log in Azure or the Admin Activity audit logs in GCP: public bucket, storage or snapshot, remote admin open to the internet, logging turned off, root use, admin granted. Each comes with who made it and, for rule-backed changes, whether a re-check confirmed the risk. Accounts without the security add-on return supported false and an empty list. Requires the security:read scope and the Pro plan or above.",
6252
+ description: "List the critical configuration changes of the last 7 days in one account (up to 100, newest first), read from CloudTrail in AWS, the activity log in Azure or the Admin Activity audit logs in GCP: public bucket, storage or snapshot, remote admin open to the internet, logging turned off, root use, admin granted. Each comes with who made it and, for rule-backed changes, whether a re-check confirmed the risk. Accounts without the risk and compliance add-on return supported false and an empty list. Requires the security:read scope and the Pro plan or above.",
6170
6253
  inputSchema: {
6171
6254
  type: "object",
6172
6255
  properties: { accountId: { type: "string", description: "Account identifier, as returned by list_accounts." } },
@@ -6177,7 +6260,7 @@ var TOOLS = [
6177
6260
  }),
6178
6261
  defineTool({
6179
6262
  name: "list_security_vulnerabilities",
6180
- description: "List the vulnerabilities (CVEs) that the cloud native scanner already found in one account (Amazon Inspector or ECR basic scanning, Microsoft Defender for Cloud, GCP Artifact Analysis and VM Manager), up to 200, highest priority first. Each one has the cloud severity and an effective severity that rises when the resource is exposed to the internet, has a known exploit or is unused according to a cost rule (low CPU is shown as context only). AAA Sentinel does not scan on its own: tools shows whether each scanner is active, absent or could not be read, truncated whether the cloud has more than the cap, and partial why the list may be incomplete (scanner missing in some regions or resource types listed in uncovered, failed reads, time budget, unreadable entries, expired rows). Amazon Inspector only sees image use on ECS and EKS: when the account has container Lambdas, App Runner services or images not in use there, uncovered.gaps says ecr_images_outside_ecs_eks and those images come last with imageInUse false. An account without the security add-on returns access teaser and an empty list, which does not mean it has no vulnerabilities. bySeverity counts the effective severity. Requires the security:read scope and the Pro plan or above.",
6263
+ description: "List the vulnerabilities (CVEs) that the cloud native scanner already found in one account (Amazon Inspector or ECR basic scanning, Microsoft Defender for Cloud, GCP Artifact Analysis and VM Manager), up to 200, highest priority first. Each one has the cloud severity and an effective severity that rises when the resource is exposed to the internet, has a known exploit or is unused according to a cost rule (low CPU is shown as context only). AAA Sentinel does not scan on its own: tools shows whether each scanner is active, absent or could not be read, truncated whether the cloud has more than the cap, and partial why the list may be incomplete (scanner missing in some regions or resource types listed in uncovered, failed reads, time budget, unreadable entries, expired rows). Amazon Inspector only sees image use on ECS and EKS: when the account has container Lambdas, App Runner services or images not in use there, uncovered.gaps says ecr_images_outside_ecs_eks and those images come last with imageInUse false. An account without the risk and compliance add-on returns access teaser and an empty list, which does not mean it has no vulnerabilities. bySeverity counts the effective severity. Requires the security:read scope and the Pro plan or above.",
6181
6264
  inputSchema: {
6182
6265
  type: "object",
6183
6266
  properties: { accountId: { type: "string", description: "Account identifier, as returned by list_accounts." } },
@@ -6186,6 +6269,17 @@ var TOOLS = [
6186
6269
  argsSchema: object({ accountId: string2().min(1) }),
6187
6270
  handler: async ({ accountId }, config2) => apiGet("/v1/security/vulnerabilities", { accountId }, config2)
6188
6271
  }),
6272
+ defineTool({
6273
+ name: "get_security_tool_spend",
6274
+ description: "Get what one account pays for the cloud's own security tools (AWS Security Hub, GuardDuty, Inspector, Config, WAF and CloudTrail; Microsoft Defender for Cloud and Azure WAF; GCP Security Command Center and Cloud Armor) this month so far and last month, and what AAA Sentinel does with each one: imports and deduplicates its findings, uses it as a data source, checks it, or does not use it for its diagnosis. Answers whether you pay twice for the same thing: overlap counts own findings the tool also reports (corroborated) and findings only the tool brings (importedOnly; capped means at least). Each tool has current and previous ({ spendUsd, costScannedAt }, null when that month has no real bill) and billing.current/previous carry totalSpendUsd (totalPartial when some tool has no amount). Amounts are in USD from the real bill of the latest cost scan of each month; spendUsd 0 means the bill read has no charge for that service, and null means it is not billed separately or its bill name is not confirmed and does not appear. state null means no state is available (no scan, an older scan, or a tool whose state is not read). CloudTrail is the whole service (management and data events together); Azure WAF (azurewaf) and Cloud Armor have no line of their own in the bill. On AWS the payer account sees the whole organization and on GCP the whole billing account (billing.scope). That AAA Sentinel does not use a tool does not mean it can be turned off. Requires the security:read scope, the Pro plan or above and the risk and compliance add-on on the account.",
6275
+ inputSchema: {
6276
+ type: "object",
6277
+ properties: { accountId: { type: "string", description: "Account identifier, as returned by list_accounts." } },
6278
+ required: ["accountId"]
6279
+ },
6280
+ argsSchema: object({ accountId: string2().min(1) }),
6281
+ handler: async ({ accountId }, config2) => apiGet("/v1/security/tools", { accountId }, config2)
6282
+ }),
6189
6283
  defineTool({
6190
6284
  name: "get_security_conformance",
6191
6285
  description: "Get CIS benchmark coverage for one account, or for every account of a managed client (clientId, Business plan). Pass exactly one of accountId or clientId. Requires the security:read scope, plus reports:read when using clientId.",
@@ -6201,6 +6295,26 @@ var TOOLS = [
6201
6295
  }),
6202
6296
  handler: async ({ accountId, clientId: clientId2 }, config2) => apiGet("/v1/security/conformance", { accountId, clientId: clientId2 }, config2)
6203
6297
  }),
6298
+ defineTool({
6299
+ name: "get_security_posture_history",
6300
+ description: "Get how the security posture changed over 30, 90 or 365 days (default 90) for one account, or for every account with the risk and compliance add-on of a managed client (clientId, Business plan, up to 20 accounts; omitted counts the rest). Returns points, one per UTC day (the last scan of that day: score, scored/total rules, open findings by severity, events), and summary: from/to (Score of the first and last point), resolved (distinct findings gone in a scan that fully evaluated their rule), opened (new, from rules already fully evaluated before), surfaced (new but not comparable: rule not fully evaluated before, or an external tool), unverified (gone without confirmation), meanFixDays by severity, comparable and notComparableReasons. When comparable is false, do not describe the Score change as the account getting better or worse. Points keep the Score computed each day, without discounting risks accepted later (acceptedRisks). Each point also has exposedCost (monthly spend on resources with critical or high findings, or null without cost data), and the summary has exposedCost (from/to) and comboSavings (monthly spend no longer paid after exposed unused resources were deleted, counted only when the resource stopped existing in a scan that fully evaluated its rules, at the last known amount of each resource). Each point uses the cost scan that was current when that security scan ran; amounts are estimates (list price or what the resource was billed), in USD and approximate. Pass exactly one of accountId or clientId. Requires the security:read scope, plus reports:read when using clientId.",
6301
+ inputSchema: {
6302
+ type: "object",
6303
+ properties: {
6304
+ accountId: { type: "string", description: "Account identifier (use this or clientId)." },
6305
+ clientId: { type: "string", description: "Managed client identifier (use this or accountId)." },
6306
+ days: { type: "number", enum: [30, 90, 365], description: "Period in days (optional, default 90)." }
6307
+ }
6308
+ },
6309
+ argsSchema: object({
6310
+ accountId: string2().min(1).optional(),
6311
+ clientId: string2().min(1).optional(),
6312
+ days: union([literal(30), literal(90), literal(365)]).optional()
6313
+ }).refine((a) => Boolean(a.accountId) !== Boolean(a.clientId), {
6314
+ message: "Pass exactly one of accountId or clientId"
6315
+ }),
6316
+ handler: async ({ accountId, clientId: clientId2, days }, config2) => apiGet("/v1/security/posture-history", { accountId, clientId: clientId2, days: days === void 0 ? void 0 : String(days) }, config2)
6317
+ }),
6204
6318
  // ── Cuentas y costes ───────────────────────────────────────
6205
6319
  defineTool({
6206
6320
  name: "get_account",
@@ -6537,7 +6651,7 @@ function findTool(name) {
6537
6651
 
6538
6652
  // src/protocol.ts
6539
6653
  var SERVER_NAME = "aaa-sentinel-mcp";
6540
- var SERVER_VERSION = "0.7.0";
6654
+ var SERVER_VERSION = "0.9.0";
6541
6655
  var SUPPORTED_PROTOCOL_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
6542
6656
  var PARSE_ERROR = -32700;
6543
6657
  var INVALID_REQUEST = -32600;
package/package.json CHANGED
@@ -1,45 +1,45 @@
1
- {
2
- "name": "@aaa-sentinel/mcp-server",
3
- "version": "0.7.0",
4
- "description": "Read-only MCP server for AAA Sentinel: query cloud costs, savings, security posture, AI spend, uptime monitoring, the remediation agent and managed clients from an AI assistant",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "aaa-sentinel-mcp": "dist/cli.js"
9
- },
10
- "files": [
11
- "dist"
12
- ],
13
- "keywords": [
14
- "mcp",
15
- "modelcontextprotocol",
16
- "finops",
17
- "cost",
18
- "monitoring",
19
- "uptime",
20
- "security",
21
- "ai-spend"
22
- ],
23
- "publishConfig": {
24
- "access": "public"
25
- },
26
- "homepage": "https://app.aaa-sentinel.com/developer/docs#mcp",
27
- "bugs": {
28
- "url": "https://app.aaa-sentinel.com/contact"
29
- },
30
- "engines": {
31
- "node": ">=20"
32
- },
33
- "scripts": {
34
- "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/cli.js",
35
- "prepublishOnly": "npm run build",
36
- "test": "vitest run"
37
- },
38
- "devDependencies": {
39
- "@types/node": "^22.0.0",
40
- "esbuild": "^0.24.0",
41
- "typescript": "^5.7.0",
42
- "vitest": "^3.2.0",
43
- "zod": "^4.6.7"
44
- }
45
- }
1
+ {
2
+ "name": "@aaa-sentinel/mcp-server",
3
+ "version": "0.9.0",
4
+ "description": "Read-only MCP server for AAA Sentinel: query cloud costs, savings, security posture, AI spend, uptime monitoring, the remediation agent and managed clients from an AI assistant",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "aaa-sentinel-mcp": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "keywords": [
14
+ "mcp",
15
+ "modelcontextprotocol",
16
+ "finops",
17
+ "cost",
18
+ "monitoring",
19
+ "uptime",
20
+ "security",
21
+ "ai-spend"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "homepage": "https://app.aaa-sentinel.com/developer/docs#mcp",
27
+ "bugs": {
28
+ "url": "https://app.aaa-sentinel.com/contact"
29
+ },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "scripts": {
34
+ "build": "esbuild src/cli.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/cli.js",
35
+ "prepublishOnly": "npm run build",
36
+ "test": "vitest run"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^22.0.0",
40
+ "esbuild": "^0.24.0",
41
+ "typescript": "^5.7.0",
42
+ "vitest": "^3.2.0",
43
+ "zod": "^4.6.7"
44
+ }
45
+ }