@lexq/cli 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -46,7 +46,7 @@ Detailed documentation lives in the `skills/` directory. **Read the relevant ski
46
46
  2. **Always dry-run before publishing.** Run `lexq analytics dry-run` to validate.
47
47
  3. **Never modify a non-DRAFT version.** Check version status first. Clone if needed.
48
48
  4. **Always check facts before creating rules.** Run `lexq facts list` to know available keys and types.
49
- 5. **Use `--format json` for parsing.** This is the default. Don't change it.
49
+ 5. **Leave `--format` at its default, `json`, and parse the output.** A command that writes a file chooses the file's format with `--as`, not `--format`.
50
50
  6. **Copy full UUIDs from output.** Never guess or truncate IDs.
51
51
  7. **Memo is required for all deploy operations.** `publish`, `live`, `rollback`, `undeploy` — every one needs `--memo`.
52
52
  8. **Handle errors gracefully.** Check the error code and follow the action table in `lexq-shared/SKILL.md`.
package/README.md CHANGED
@@ -224,13 +224,15 @@ lexq webhook-subscriptions list | get | save | delete | test
224
224
 
225
225
  | Flag | Description |
226
226
  |--------------------------|--------------------------------------------|
227
- | `--format <json\|table>` | Output format (default: `json`) |
227
+ | `--format <json\|table>` | How results are printed (default: `json`) |
228
228
  | `--api-key <key>` | Override stored API key |
229
229
  | `--base-url <url>` | Override API base URL |
230
230
  | `--dry-run` | Preview the HTTP request without executing |
231
231
  | `--verbose` | Show request/response details |
232
232
  | `--no-color` | Disable colored output |
233
233
 
234
+ `--format` only changes how results are printed. A command that writes a file, such as `lexq facts export`, chooses the file's format with `--as csv` or `--as json`.
235
+
234
236
  ## Configuration
235
237
 
236
238
  Config is stored at `~/.lexq/config.json`:
package/dist/index.js CHANGED
@@ -184,6 +184,14 @@ async function apiRequestWithMeta(method, path, options = {}) {
184
184
 
185
185
  // src/lib/output.ts
186
186
  import Table from "cli-table3";
187
+ import { InvalidArgumentError } from "commander";
188
+ var OUTPUT_FORMATS = ["json", "table"];
189
+ function parseOutputFormat(value) {
190
+ if (OUTPUT_FORMATS.includes(value)) return value;
191
+ throw new InvalidArgumentError(
192
+ `Use ${OUTPUT_FORMATS.join(" or ")}. The format of an exported file is chosen with --as.`
193
+ );
194
+ }
187
195
  function printJson(data) {
188
196
  console.log(stringifyJson(data, 2));
189
197
  }
@@ -394,6 +402,7 @@ var PURGE_AFTER_DAYS = 30;
394
402
  var DESCRIPTION_MAX_LENGTH = 255;
395
403
  var COMMIT_MESSAGE_MAX_LENGTH = 500;
396
404
  var SCHEDULE_TICK_MAX_MILLIS = 6e4;
405
+ var MAX_DRY_RUN_EVALUATIONS_PER_MINUTE = 60;
397
406
 
398
407
  // src/commands/groups.ts
399
408
  import "commander";
@@ -1346,6 +1355,7 @@ function escapeUnsafe(json) {
1346
1355
 
1347
1356
  // src/lib/export.ts
1348
1357
  import { writeFileSync as writeFileSync2 } from "fs";
1358
+ import { extname } from "path";
1349
1359
  var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
1350
1360
  function parseFormat(value) {
1351
1361
  if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
@@ -1355,6 +1365,25 @@ function parseFormat(value) {
1355
1365
  `Unsupported export format: ${String(value)}. Use ${EXPORT_FORMATS.join(" or ")}.`
1356
1366
  );
1357
1367
  }
1368
+ function fileFormat(command) {
1369
+ const opts = command.opts();
1370
+ const format = parseFormat(opts.as);
1371
+ const extension = typeof opts.output === "string" ? extname(opts.output).slice(1).toLowerCase() : "";
1372
+ let root = command;
1373
+ while (root.parent) root = root.parent;
1374
+ if (EXPORT_FORMATS.includes(extension)) {
1375
+ if (extension !== format) {
1376
+ console.error(
1377
+ `Note: ${opts.output} will hold ${format}. Pass --as ${extension} to write ${extension}.`
1378
+ );
1379
+ }
1380
+ } else if (root.getOptionValueSource("format") === "cli" && command.getOptionValueSource("as") !== "cli") {
1381
+ console.error(
1382
+ `Note: --format sets how results are printed, not the file. This file is ${format}; choose ${EXPORT_FORMATS.join(" or ")} with --as.`
1383
+ );
1384
+ }
1385
+ return format;
1386
+ }
1358
1387
  async function runExport(path, format, options) {
1359
1388
  const { output, params, ...clientOptions } = options;
1360
1389
  const body = await apiRequest("GET", path, {
@@ -1621,10 +1650,10 @@ ${warning}
1621
1650
  $ lexq facts export --as json --output facts.json
1622
1651
  $ lexq facts export --keyword user --as json
1623
1652
  `
1624
- ).action(async (opts) => {
1653
+ ).action(async (opts, command) => {
1625
1654
  try {
1626
1655
  const globalOpts = program.opts();
1627
- await runExport("schema/facts/export", parseFormat(opts.as), {
1656
+ await runExport("schema/facts/export", fileFormat(command), {
1628
1657
  apiKey: globalOpts.apiKey,
1629
1658
  baseUrl: globalOpts.baseUrl,
1630
1659
  dryRun: globalOpts.dryRun,
@@ -2208,13 +2237,13 @@ import "commander";
2208
2237
  import dedent8 from "dedent";
2209
2238
 
2210
2239
  // src/lib/dataset-upload.ts
2211
- import { extname } from "path";
2240
+ import { extname as extname2 } from "path";
2212
2241
  var DATASET_CONTENT_TYPES = {
2213
2242
  ".csv": "text/csv",
2214
2243
  ".json": "application/json"
2215
2244
  };
2216
2245
  function datasetContentType(filename) {
2217
- return DATASET_CONTENT_TYPES[extname(filename).toLowerCase()];
2246
+ return DATASET_CONTENT_TYPES[extname2(filename).toLowerCase()];
2218
2247
  }
2219
2248
  function uploadDataset(bytes, filename, options) {
2220
2249
  const form = new FormData();
@@ -2226,6 +2255,9 @@ function uploadDataset(bytes, filename, options) {
2226
2255
  });
2227
2256
  }
2228
2257
 
2258
+ // src/lib/dry-run-limit.ts
2259
+ var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007) until the next minute starts.`;
2260
+
2229
2261
  // src/commands/analytics.ts
2230
2262
  function registerAnalyticsCommands(program) {
2231
2263
  const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
@@ -2255,6 +2287,8 @@ function registerAnalyticsCommands(program) {
2255
2287
  $ lexq analytics dry-run --version-id <vid> --file test-input.json
2256
2288
 
2257
2289
  The request body must include a "facts" object. Use --debug for execution traces
2290
+
2291
+ ${DRY_RUN_LIMIT_NOTE}
2258
2292
  `
2259
2293
  ).action(async (opts) => {
2260
2294
  try {
@@ -2293,6 +2327,8 @@ function registerAnalyticsCommands(program) {
2293
2327
  }'
2294
2328
 
2295
2329
  Shows side-by-side which rules matched and what actions fired for each version.
2330
+
2331
+ ${DRY_RUN_LIMIT_NOTE}
2296
2332
  `
2297
2333
  ).action(async (opts) => {
2298
2334
  try {
@@ -2604,10 +2640,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2604
2640
  $ lexq analytics simulation export --id <simId> --as csv --output results.csv
2605
2641
  $ lexq analytics simulation export --id <simId> --as json
2606
2642
  `
2607
- ).action(async (opts) => {
2643
+ ).action(async (opts, command) => {
2608
2644
  try {
2609
2645
  const globalOpts = program.opts();
2610
- await runExport(`analytics/simulations/${opts.id}/export`, parseFormat(opts.as), {
2646
+ await runExport(`analytics/simulations/${opts.id}/export`, fileFormat(command), {
2611
2647
  apiKey: globalOpts.apiKey,
2612
2648
  baseUrl: globalOpts.baseUrl,
2613
2649
  dryRun: globalOpts.dryRun,
@@ -2666,7 +2702,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2666
2702
  $ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
2667
2703
  $ lexq analytics dataset template --group-id <gid> --version-id <vid> --as json
2668
2704
  `
2669
- ).action(async (opts) => {
2705
+ ).action(async (opts, command) => {
2670
2706
  try {
2671
2707
  const globalOpts = program.opts();
2672
2708
  const config = loadConfig();
@@ -2675,7 +2711,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2675
2711
  if (!apiKey) {
2676
2712
  throw new Error('Not authenticated. Run "lexq auth login" first.');
2677
2713
  }
2678
- const fmt = parseFormat(opts.as);
2714
+ const fmt = fileFormat(command);
2679
2715
  const url = new URL(
2680
2716
  `analytics/groups/${opts.groupId}/versions/${opts.versionId}/dataset-template`,
2681
2717
  baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
@@ -3007,8 +3043,10 @@ function registerReplayCommands(program) {
3007
3043
  "after",
3008
3044
  dedent11`
3009
3045
 
3010
- Free of charge (TPS throttle only). Returns decisionChanged, effect
3011
- changes, fired rules on both sides, and a determinism verdict.
3046
+ Returns decisionChanged, effect changes, fired rules on both sides,
3047
+ and a determinism verdict.
3048
+
3049
+ ${DRY_RUN_LIMIT_NOTE}
3012
3050
 
3013
3051
  Example:
3014
3052
  $ lexq replay decision --trace-id <tid> --version-id <vid>
@@ -3140,10 +3178,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
3140
3178
  $ lexq replay export --id <jobId> --as csv --output blast-radius.csv
3141
3179
  $ lexq replay export --id <jobId> --as json
3142
3180
  `
3143
- ).action(async (opts) => {
3181
+ ).action(async (opts, command) => {
3144
3182
  try {
3145
3183
  const globalOpts = program.opts();
3146
- await runExport(`replay/jobs/${opts.id}/export`, parseFormat(opts.as), {
3184
+ await runExport(`replay/jobs/${opts.id}/export`, fileFormat(command), {
3147
3185
  apiKey: globalOpts.apiKey,
3148
3186
  baseUrl: globalOpts.baseUrl,
3149
3187
  dryRun: globalOpts.dryRun,
@@ -3684,7 +3722,7 @@ function registerAnalyticsTools(server, callApi) {
3684
3722
  title: "Dry Run",
3685
3723
  annotations: READ_ONLY,
3686
3724
  description: dedent15`
3687
- Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
3725
+ Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects. ${DRY_RUN_LIMIT_NOTE}
3688
3726
 
3689
3727
  Returns:
3690
3728
  inputFacts — normalized input facts
@@ -3713,7 +3751,7 @@ function registerAnalyticsTools(server, callApi) {
3713
3751
  title: "Dry Run Compare",
3714
3752
  annotations: READ_ONLY,
3715
3753
  description: dedent15`
3716
- Compare dry run results between two versions using the same input facts. Useful for validating changes.
3754
+ Compare dry run results between two versions using the same input facts. Useful for validating changes. ${DRY_RUN_LIMIT_NOTE}
3717
3755
 
3718
3756
  Returns:
3719
3757
  resultA / resultB — full DryRunResponse for each version
@@ -4830,7 +4868,7 @@ function registerReplayTools(server, callApi) {
4830
4868
  {
4831
4869
  title: "Replay a Decision",
4832
4870
  annotations: READ_ONLY,
4833
- description: "Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous and free of charge (TPS throttle only). A replay sends no webhook, notification, or event: rule actions produce no outward effects.",
4871
+ description: `Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous. ${DRY_RUN_LIMIT_NOTE} A replay sends no webhook, notification, or event: rule actions produce no outward effects.`,
4834
4872
  inputSchema: z10.object({
4835
4873
  traceId: z10.string().describe("Trace ID of the past execution to replay"),
4836
4874
  candidateVersionId: z10.string().uuid().describe("Version to re-evaluate against")
@@ -5428,7 +5466,12 @@ function createCli() {
5428
5466
  const program = new Command();
5429
5467
  program.name("lexq").description(
5430
5468
  "Command-line interface for the LexQ Decision Operations Platform. Manage policies, run Impact Simulation, and deploy from your terminal."
5431
- ).version(getVersion2(), "-V, --version").option("--format <format>", "Output format: json or table", "json").option("--api-key <key>", "Override stored API key").option(
5469
+ ).version(getVersion2(), "-V, --version").option(
5470
+ "--format <format>",
5471
+ `How results are printed: ${OUTPUT_FORMATS.join(" or ")}`,
5472
+ parseOutputFormat,
5473
+ "json"
5474
+ ).option("--api-key <key>", "Override stored API key").option(
5432
5475
  "--base-url <url>",
5433
5476
  "Override API base URL \u2014 must include the API prefix, e.g. http://localhost:8080/api/v1/partners"
5434
5477
  ).option("--dry-run", "Preview the HTTP request without executing").option("--verbose", "Show request/response details").option("--no-color", "Disable colored output");
@@ -219,6 +219,7 @@ var STATS_DEFAULT_WINDOW_DAYS = 30;
219
219
  var STATS_MAX_WINDOW_DAYS = 31;
220
220
  var FAILURE_LOG_LOOKBACK_DAYS = 7;
221
221
  var PURGE_AFTER_DAYS = 30;
222
+ var MAX_DRY_RUN_EVALUATIONS_PER_MINUTE = 60;
222
223
 
223
224
  // src/mcp/tools/groups.ts
224
225
  function registerGroupTools(server, callApi) {
@@ -652,6 +653,7 @@ var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
652
653
 
653
654
  // src/lib/export.ts
654
655
  import { writeFileSync as writeFileSync2 } from "fs";
656
+ import { extname } from "path";
655
657
  var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
656
658
 
657
659
  // src/mcp/tools/facts.ts
@@ -977,6 +979,11 @@ function registerDeployTools(server, callApi) {
977
979
  // src/mcp/tools/analytics.ts
978
980
  import { z as z7 } from "zod";
979
981
  import dedent3 from "dedent";
982
+
983
+ // src/lib/dry-run-limit.ts
984
+ var DRY_RUN_LIMIT_NOTE = `Not billed and not counted toward the plan's usage. Dry runs, dry run compares, and single-decision replays share a limit of ${MAX_DRY_RUN_EVALUATIONS_PER_MINUTE} evaluations per minute for each organization; a compare counts as two. Past the limit, a call fails with 429 (C-007) until the next minute starts.`;
985
+
986
+ // src/mcp/tools/analytics.ts
980
987
  function datasetBlock(path) {
981
988
  const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
982
989
  return {
@@ -991,7 +998,7 @@ function registerAnalyticsTools(server, callApi) {
991
998
  title: "Dry Run",
992
999
  annotations: READ_ONLY,
993
1000
  description: dedent3`
994
- Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
1001
+ Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects. ${DRY_RUN_LIMIT_NOTE}
995
1002
 
996
1003
  Returns:
997
1004
  inputFacts — normalized input facts
@@ -1020,7 +1027,7 @@ function registerAnalyticsTools(server, callApi) {
1020
1027
  title: "Dry Run Compare",
1021
1028
  annotations: READ_ONLY,
1022
1029
  description: dedent3`
1023
- Compare dry run results between two versions using the same input facts. Useful for validating changes.
1030
+ Compare dry run results between two versions using the same input facts. Useful for validating changes. ${DRY_RUN_LIMIT_NOTE}
1024
1031
 
1025
1032
  Returns:
1026
1033
  resultA / resultB — full DryRunResponse for each version
@@ -1295,7 +1302,7 @@ function registerReplayTools(server, callApi) {
1295
1302
  {
1296
1303
  title: "Replay a Decision",
1297
1304
  annotations: READ_ONLY,
1298
- description: "Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous and free of charge (TPS throttle only). A replay sends no webhook, notification, or event: rule actions produce no outward effects.",
1305
+ description: `Re-evaluate a past execution (traceId) against a candidate version and return the decision diff (decisionChanged, effect changes, fired rules) plus a determinism verdict. Synchronous. ${DRY_RUN_LIMIT_NOTE} A replay sends no webhook, notification, or event: rule actions produce no outward effects.`,
1299
1306
  inputSchema: z9.object({
1300
1307
  traceId: z9.string().describe("Trace ID of the past execution to replay"),
1301
1308
  candidateVersionId: z9.string().uuid().describe("Version to re-evaluate against")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "mcpName": "io.lexq/lexq",
5
5
  "description": "The Decision Operations Platform for engineering teams",
6
6
  "type": "module",
@@ -47,6 +47,7 @@
47
47
  "test:rollback": "pnpm build && node tests/rollback.mjs",
48
48
  "test:dataset-upload": "pnpm build && node tests/dataset-upload.mjs",
49
49
  "test:export-limit": "pnpm build && node tests/export-limit.mjs",
50
+ "test:format-options": "pnpm build && node tests/format-options.mjs",
50
51
  "prepublishOnly": "pnpm build",
51
52
  "knip": "knip",
52
53
  "format": "prettier --write \"src/**/*.ts\"",
@@ -72,16 +72,19 @@ Every command accepts these flags:
72
72
 
73
73
  | Flag | Description | Default |
74
74
  |--------------------------|--------------------------------------------|---------------------------------------|
75
- | `--format <json\|table>` | Output format | `json` |
75
+ | `--format <json\|table>` | How results are printed | `json` |
76
76
  | `--api-key <key>` | Override stored API key | from config |
77
77
  | `--base-url <url>` | Override API base URL | `https://api.lexq.io/api/v1/partners` |
78
78
  | `--dry-run` | Preview the HTTP request without executing | off |
79
79
  | `--verbose` | Show request/response details | off |
80
80
  | `--no-color` | Disable colored output | off |
81
81
 
82
- **Agent best practice:** Always use `--format json` (the default). Parse JSON output programmatically. Use
82
+ **Agent best practice:** Leave `--format` at its default, `json`, and parse the output programmatically. Use
83
83
  `--format table` only when displaying to humans.
84
84
 
85
+ `--format` does not change a file. The commands that write one (`facts export`, `analytics simulation export`,
86
+ `replay export`, `analytics dataset template`) choose its format with `--as csv` or `--as json`.
87
+
85
88
  ## Command Groups
86
89
 
87
90
  | Group | Commands | Description |