@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 +1 -1
- package/README.md +3 -1
- package/dist/index.js +59 -16
- package/dist/mcp/register.js +10 -3
- package/package.json +2 -1
- package/skills/lexq-shared/SKILL.md +5 -2
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. **
|
|
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>` |
|
|
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",
|
|
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[
|
|
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`,
|
|
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 =
|
|
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
|
-
|
|
3011
|
-
|
|
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`,
|
|
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:
|
|
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(
|
|
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");
|
package/dist/mcp/register.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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>` |
|
|
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:**
|
|
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 |
|