@lexq/cli 1.1.7 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,12 @@ import { Implementation, McpServer } from '@modelcontextprotocol/server';
2
2
  import { LosslessNumber } from 'lossless-json';
3
3
  export { LosslessNumber } from 'lossless-json';
4
4
 
5
+ declare class ApiError extends Error {
6
+ readonly statusCode: number;
7
+ readonly errorCode: string | null;
8
+ constructor(statusCode: number, errorCode: string | null, message: string);
9
+ }
10
+
5
11
  interface McpToolResult {
6
12
  [key: string]: unknown;
7
13
  content: Array<{
@@ -61,12 +67,6 @@ declare function formatUnregisteredFactWarning(meta: unknown): string | null;
61
67
  */
62
68
  declare function formatValueDomainMismatchWarning(meta: unknown): string | null;
63
69
 
64
- declare class ApiError extends Error {
65
- readonly statusCode: number;
66
- readonly errorCode: string | null;
67
- constructor(statusCode: number, errorCode: string | null, message: string);
68
- }
69
-
70
70
  /** Parses JSON text without rounding numbers that a double cannot hold. */
71
71
  declare const parseJson: (text: string) => unknown;
72
72
  /**
@@ -84,7 +84,7 @@ function registerGroupTools(server, callApi) {
84
84
  {
85
85
  title: "Get Policy Group",
86
86
  annotations: { readOnlyHint: true },
87
- description: "Get a single policy group by ID.",
87
+ description: "Get a single policy group by ID. Its currentVersionId and currentDeploymentId are what lexq_deploy_rollback expects, and previousVersionName is the version a rollback restores.",
88
88
  inputSchema: z2.object({
89
89
  groupId: z2.string().uuid().describe("Policy group ID")
90
90
  })
@@ -778,6 +778,7 @@ function registerFactTools(server, callApi) {
778
778
  }
779
779
 
780
780
  // src/mcp/tools/deploy.ts
781
+ import dedent2 from "dedent";
781
782
  import { z as z6 } from "zod";
782
783
  function registerDeployTools(server, callApi) {
783
784
  server.registerTool(
@@ -815,14 +816,20 @@ function registerDeployTools(server, callApi) {
815
816
  {
816
817
  title: "Rollback Deployment",
817
818
  annotations: { readOnlyHint: false, destructiveHint: true },
818
- description: "Rollback to the previous deployed version. Only available if there is a previous version that is still ACTIVE and whose effective end has not passed (P-058).",
819
+ description: dedent2`
820
+ Roll the group back to the version that was live before the current one. Call lexq_groups_get first: currentVersionName is live now, previousVersionName is what a rollback restores, and a rollback is rejected while canRollback is false. A rollback needs a previous version that is still ACTIVE and whose effective end has not passed (P-058).
821
+
822
+ Pass the currentVersionId and currentDeploymentId from that call as expectedCurrentVersionId and expectedCurrentDeploymentId. If anyone deployed, rolled back or undeployed in between, the rollback is rejected with 409 and nothing changes: P-062 if another version or no version is live, P-063 if the same version is live but from a newer deployment. Read the group again and check with the user before retrying.
823
+ `,
819
824
  inputSchema: z6.object({
820
825
  groupId: z6.string().uuid().describe("Policy group ID"),
826
+ expectedCurrentVersionId: z6.string().uuid().describe("currentVersionId from your last read of the group"),
827
+ expectedCurrentDeploymentId: z6.string().uuid().describe("currentDeploymentId from your last read of the group"),
821
828
  memo: z6.string().min(1).describe("Rollback reason (required)")
822
829
  })
823
830
  },
824
- async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
825
- body: { memo }
831
+ async ({ groupId, expectedCurrentVersionId, expectedCurrentDeploymentId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
832
+ body: { memo, expectedCurrentVersionId, expectedCurrentDeploymentId }
826
833
  })
827
834
  );
828
835
  server.registerTool(
@@ -922,7 +929,7 @@ function registerDeployTools(server, callApi) {
922
929
  {
923
930
  title: "Deployment Overview",
924
931
  annotations: { readOnlyHint: true },
925
- description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer.",
932
+ description: "Show current deployment status of all groups \u2014 which version is live, last deployment type, and deployer. Each group also carries currentDeploymentId, previousVersionName (the version a rollback restores) and canRollback, as in lexq_groups_get.",
926
933
  inputSchema: z6.object({})
927
934
  },
928
935
  async () => callApi("GET", "deployments/overview")
@@ -958,14 +965,21 @@ function registerDeployTools(server, callApi) {
958
965
 
959
966
  // src/mcp/tools/analytics.ts
960
967
  import { z as z7 } from "zod";
961
- import dedent2 from "dedent";
968
+ import dedent3 from "dedent";
969
+ function datasetBlock(path) {
970
+ const dataset = { type: "UPLOADED", source: "S3_BUCKET", path };
971
+ return {
972
+ type: "text",
973
+ text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
974
+ };
975
+ }
962
976
  function registerAnalyticsTools(server, callApi) {
963
977
  server.registerTool(
964
978
  "lexq_dry_run",
965
979
  {
966
980
  title: "Dry Run",
967
981
  annotations: { readOnlyHint: true },
968
- description: dedent2`
982
+ description: dedent3`
969
983
  Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
970
984
 
971
985
  Returns:
@@ -995,7 +1009,7 @@ function registerAnalyticsTools(server, callApi) {
995
1009
  {
996
1010
  title: "Dry Run Compare",
997
1011
  annotations: { readOnlyHint: true },
998
- description: dedent2`
1012
+ description: dedent3`
999
1013
  Compare dry run results between two versions using the same input facts. Useful for validating changes.
1000
1014
 
1001
1015
  Returns:
@@ -1033,7 +1047,7 @@ function registerAnalyticsTools(server, callApi) {
1033
1047
  {
1034
1048
  title: "Start Simulation",
1035
1049
  annotations: { readOnlyHint: false, destructiveHint: false },
1036
- description: dedent2`
1050
+ description: dedent3`
1037
1051
  Start an Impact Simulation against historical, uploaded, or inline data.
1038
1052
 
1039
1053
  dataset.type and dataset.source are BOTH required, and must be paired:
@@ -1138,13 +1152,17 @@ function registerAnalyticsTools(server, callApi) {
1138
1152
  {
1139
1153
  title: "Upload Dataset",
1140
1154
  annotations: { readOnlyHint: false, destructiveHint: false },
1141
- description: dedent2`
1155
+ description: dedent3`
1142
1156
  Upload inline CSV or JSON content as a simulation dataset.
1143
1157
  The content is uploaded to S3 and a path is returned in the "path" field.
1144
1158
 
1145
1159
  To use the returned path in lexq_simulation_start, set:
1146
1160
  dataset: { "type": "UPLOADED", "source": "S3_BUCKET", "path": "<returned path>" }
1147
1161
 
1162
+ The content travels inside the tool call, so this suits small datasets, and the hosted server at
1163
+ mcp.lexq.io limits the size of a request. For a larger file, use lexq_dataset_upload_file on the
1164
+ local server (lexq serve --mcp), or upload it with the CLI or the console and pass the returned path.
1165
+
1148
1166
  CSV example:
1149
1167
  userId,paymentAmount
1150
1168
  user_001,150000
@@ -1164,13 +1182,8 @@ function registerAnalyticsTools(server, callApi) {
1164
1182
  if (!result.isError) {
1165
1183
  try {
1166
1184
  const uploaded = parseJson(result.content[0]?.text ?? "{}");
1167
- if (uploaded.path) {
1168
- const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
1169
- result.content.push({
1170
- type: "text",
1171
- text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
1172
- });
1173
- }
1185
+ const path = uploaded.data?.path ?? uploaded.path;
1186
+ if (typeof path === "string" && path) result.content.push(datasetBlock(path));
1174
1187
  } catch {
1175
1188
  }
1176
1189
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.1.7",
3
+ "version": "2.1.0",
4
4
  "mcpName": "io.lexq/lexq",
5
- "description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
5
+ "description": "The Decision Operations Platform for engineering teams",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "lexq": "./dist/index.js"
@@ -44,6 +44,8 @@
44
44
  "test:command-options": "node tests/command-options.mjs",
45
45
  "test:value-domain": "pnpm build && node tests/value-domain.mjs",
46
46
  "test:fact-update": "pnpm build && node tests/fact-update.mjs",
47
+ "test:rollback": "pnpm build && node tests/rollback.mjs",
48
+ "test:dataset-upload": "pnpm build && node tests/dataset-upload.mjs",
47
49
  "prepublishOnly": "pnpm build",
48
50
  "knip": "knip",
49
51
  "format": "prettier --write \"src/**/*.ts\"",
@@ -57,7 +59,11 @@
57
59
  "ai-agent",
58
60
  "agent-skills",
59
61
  "simulation",
60
- "business-rules"
62
+ "business-rules",
63
+ "developer-tools",
64
+ "decision-management",
65
+ "mcp",
66
+ "mcp-server"
61
67
  ],
62
68
  "author": "LexQ, LLC",
63
69
  "license": "Apache-2.0",
@@ -71,16 +77,16 @@
71
77
  },
72
78
  "packageManager": "pnpm@10.28.0",
73
79
  "dependencies": {
74
- "@modelcontextprotocol/server": "^2.0.0",
80
+ "@modelcontextprotocol/server": "^2.3.1",
75
81
  "cli-table3": "^0.6.5",
76
82
  "commander": "^13.1.0",
77
83
  "dedent": "^1.7.2",
78
84
  "lossless-json": "4.3.1",
79
- "zod": "^4.5.4"
85
+ "zod": "^4.6.7"
80
86
  },
81
87
  "devDependencies": {
82
88
  "@eslint/js": "^9.20.0",
83
- "@modelcontextprotocol/client": "^2.0.0",
89
+ "@modelcontextprotocol/client": "^2.3.1",
84
90
  "@types/node": "^22.12.0",
85
91
  "eslint": "^10.2.0",
86
92
  "knip": "^6.29.0",
@@ -352,17 +352,24 @@ lexq rules create --group-id <gid> --version-id <vid> --json '{
352
352
  **Goal:** Something went wrong in production — revert to the previous version.
353
353
 
354
354
  ```bash
355
- # 1. Check current state
356
- lexq deploy overview
355
+ # 1. Read the group: currentVersionName is live now, previousVersionName is what a
356
+ # rollback restores, and a rollback is rejected while canRollback is false
357
+ lexq groups get --id <gid>
357
358
 
358
- # 2. Rollback
359
- lexq deploy rollback --group-id <gid> --memo "Reverting due to increased error rate"
359
+ # 2. Roll back with the IDs from step 1
360
+ lexq deploy rollback --group-id <gid> --memo "Reverting due to increased error rate" \
361
+ --expected-current-version-id <currentVersionId> \
362
+ --expected-current-deployment-id <currentDeploymentId>
360
363
 
361
364
  # 3. Verify
362
365
  lexq deploy overview
363
366
  lexq history stats
364
367
  ```
365
368
 
369
+ If anyone deployed, rolled back or undeployed after step 1, step 2 is rejected with `409` and
370
+ nothing changes: `P-062` if another version or no version is live, `P-063` if the same version is
371
+ live but from a newer deployment. Start again from step 1.
372
+
366
373
  ---
367
374
 
368
375
  ## Recipe 7: Monitoring + Failure Triage
@@ -280,6 +280,11 @@ lexq analytics simulation start --json '{
280
280
  **CSV format:** Header row with fact keys, data rows with values. Types auto-detected.
281
281
  **JSON format:** Array of objects `[{"key": "value"}, ...]`
282
282
 
283
+ Dataset files can be up to 10 MB. Over MCP, the local server (`lexq serve --mcp`) has
284
+ `lexq_dataset_upload_file`, which takes the absolute path of a `.csv` or `.json` file and reads it from
285
+ disk. The hosted server has only `lexq_dataset_upload`, which takes the content inline and suits small
286
+ datasets. For a larger file there, upload it with the command above and pass the returned `path`.
287
+
283
288
  ### Check Status (Poll)
284
289
 
285
290
  ```bash