@lexq/cli 1.1.2 → 1.1.4

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # LexQ CLI
2
2
 
3
3
  > **The Decision Operations Platform for engineering teams.**
4
- > CLI and MCP server for [LexQ](https://lexq.io) — move business rules out of
4
+ > CLI and MCP server for [LexQ](https://lexq.io). Move business rules out of
5
5
  > your deploy pipeline, prove every change with Impact Simulation, and ship without
6
6
  > a git push.
7
7
 
@@ -14,20 +14,17 @@
14
14
 
15
15
  ## Why LexQ?
16
16
 
17
- Your business rules — pricing, promotions, fee logic, eligibility checks —
18
- change often. But every change ships through the same PR → review → staging
19
- → deploy cycle as your core application code. A one-line discount rule
20
- takes two weeks.
17
+ Your business rules, such as pricing, promotions, fee logic, and eligibility
18
+ checks, change often. But every change ships through the same PR → review →
19
+ staging → deploy cycle as your core application code, so even a small change
20
+ waits for the next release.
21
21
 
22
- This is an architectural problem, not a process problem. Business logic
23
- that changes weekly shouldn't live in code that ships quarterly.
22
+ **LexQ keeps those rules out of your code.** Your service calls the LexQ API to
23
+ evaluate them. You change rules in a visual console or through this CLI, test
24
+ each change with Impact Simulation against past executions or a dataset you
25
+ upload, and deploy them without an application release.
24
26
 
25
- **LexQ separates the two.** Rules live outside your application. You change
26
- them in a visual console or through this CLI. You prove every change with
27
- Impact Simulation against real execution data. You deploy without touching
28
- your codebase.
29
-
30
- Every decision leaves a full audit trace.
27
+ Every decision leaves a full audit trail.
31
28
 
32
29
  ## Install
33
30
 
@@ -67,19 +64,22 @@ lexq deploy publish --group-id <GROUP_ID> --version-id <VERSION_ID> --memo "v1"
67
64
  lexq deploy live --group-id <GROUP_ID> --version-id <VERSION_ID> --memo "Initial deploy"
68
65
  ```
69
66
 
70
- ## For AI Agents — the Complete Partner API
67
+ ## For AI Agents
68
+
69
+ Claude, Cursor, and other MCP-compatible agents can use the Model Context
70
+ Protocol to manage policy groups, versions, rules, and facts, run simulations
71
+ and replays, deploy versions, and read execution history.
71
72
 
72
- LexQ is designed to be AI-native. The entire policy engine Partner API is
73
- exposed via Model Context Protocol. Claude, Cursor, and other MCP-compatible
74
- agents can create, simulate, and deploy rules autonomously, with human approval
75
- before production.
73
+ Approval happens in your MCP client, which decides which tool calls you confirm.
74
+ The deploy, rollback, and undeploy tools are marked as destructive, so a client
75
+ can ask you before running them.
76
76
 
77
- ### Claude.ai (Cloud — no install)
77
+ ### Claude.ai (cloud, no install)
78
78
 
79
- 1. **Settings → Connectors → Add Custom Integration**
80
- 2. Enter: `https://mcp.lexq.io`
81
- 3. Sign in with your LexQ account and select an API key
82
- 4. Done — the full toolset available in every conversation
79
+ 1. **Customize → Connectors → + Add → Add custom connector**
80
+ 2. Name it, enter `https://mcp.lexq.io`, and choose **Sign in now**
81
+ 3. On the LexQ page that opens, sign in and generate a new API key for MCP, or choose **I already have an API key** and paste an existing one
82
+ 4. The tools are now available in every conversation
83
83
 
84
84
  ### Remote (Streamable HTTP)
85
85
 
@@ -170,8 +170,8 @@ keeps working.
170
170
 
171
171
  ## AI Agent Skills
172
172
 
173
- LexQ CLI ships with **AI Agent Skills** — structured documentation agents
174
- read to understand *how* to use the tools, not just *what* they are.
173
+ LexQ CLI ships with **AI Agent Skills**: structured documentation that tells
174
+ agents *how* to use each tool.
175
175
 
176
176
  ```
177
177
  skills/
@@ -242,9 +242,9 @@ Config is stored at `~/.lexq/config.json`:
242
242
  }
243
243
  ```
244
244
 
245
- The file holds an API key in plain text, so it is written `0600` inside a `0700` directory —
246
- readable by its owner and nobody else on the machine. An install created by an older version
247
- is repaired on the next save.
245
+ The file holds an API key in plain text. It is saved with mode `0600` inside a `0700`
246
+ directory, so only its owner can read it. If an older version created it with looser
247
+ permissions, the CLI tightens them the next time it saves the file.
248
248
 
249
249
  ### Environment variables
250
250
 
@@ -280,7 +280,7 @@ pnpm enums:check # Generated enums match the engine contract
280
280
  pnpm prose:check # Numbers and code tables in prose match the contract
281
281
  pnpm ab-key:check # Tool descriptions still name the A/B traffic key
282
282
  pnpm surface:check # Nothing internal reached a public file
283
- pnpm registry:check # server.json agrees with package.json
283
+ pnpm registry:check # server.json agrees with package.json and src/mcp/server-info.json
284
284
  pnpm schemas:check # tools/list matches the recorded snapshot (needs a build)
285
285
  pnpm test:fact-key # Fact key grammar
286
286
  pnpm test:decimals # Metric precision display
package/dist/index.js CHANGED
@@ -1058,7 +1058,7 @@ ${data.length} total`);
1058
1058
  Action Types:
1059
1059
  MUTATE_FACT { targetVar, operator, method, operand, refVar?, rounding? }
1060
1060
  SET_FACT { targetVar, value }
1061
- BLOCK { reason }
1061
+ BLOCK { reason? }
1062
1062
 
1063
1063
  refVar is the percentage base and is optional — omit to use targetVar itself.
1064
1064
  It is only valid in PERCENTAGE × {ASSIGN, ADD, SUB}.
@@ -1302,7 +1302,8 @@ function registerFactCommands(program) {
1302
1302
  delete Remove a fact definition
1303
1303
  action-metadata Show action runtime fact metadata
1304
1304
 
1305
- System facts (userId, userTags) are auto-created.
1305
+ Every tenant starts with userId and userTags, and with the system facts
1306
+ isBlocked and blockReason, which only the BLOCK action writes.
1306
1307
  `
1307
1308
  );
1308
1309
  facts.command("list").description("List fact definitions").option("--keyword <keyword>", "Filter by keyword").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
@@ -1443,7 +1444,8 @@ ${data.length} unregistered`);
1443
1444
  "after",
1444
1445
  dedent6`
1445
1446
 
1446
- System facts accept name, description, and the PII flag only.
1447
+ System facts (isBlocked, blockReason) accept changes to name, description, the PII flag,
1448
+ and the value domain. --required is ignored, and the type cannot change (FD-003).
1447
1449
 
1448
1450
  Examples:
1449
1451
  $ lexq facts update --id <factId> --name "Updated Name" --required
@@ -3562,6 +3564,20 @@ function formatUnregisteredFactWarning(meta) {
3562
3564
  ].join("\n");
3563
3565
  }
3564
3566
 
3567
+ // src/mcp/server-info.json
3568
+ var server_info_default = {
3569
+ title: "LexQ",
3570
+ description: "The Decision Operations Platform for engineering teams",
3571
+ websiteUrl: "https://lexq.io",
3572
+ icons: [
3573
+ {
3574
+ src: "https://lexq.io/logo.png",
3575
+ mimeType: "image/png",
3576
+ sizes: ["1024x1024"]
3577
+ }
3578
+ ]
3579
+ };
3580
+
3565
3581
  // src/mcp/tools/status.ts
3566
3582
  import { z } from "zod";
3567
3583
  function registerStatusTools(server, callApi) {
@@ -3925,11 +3941,21 @@ function registerRuleTools(server, callApi) {
3925
3941
  requires operand !== 0.
3926
3942
  - SET_FACT: { targetVar: string, value: string|number|boolean } targetVar must be a
3927
3943
  registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
3928
- the only action that can write a key absent from the input facts. MUTATE_FACT requires
3944
+ the only action whose targetVar may be absent from the input facts. MUTATE_FACT requires
3929
3945
  its target to be present in the input facts.
3930
- - BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
3946
+ - BLOCK: { reason?: string } Records a rejection decision. It does NOT halt rule execution —
3931
3947
  subsequent actions and subsequent winning rules still run. Enforcement is the caller's
3932
- responsibility; the decision surfaces as the isBlocked fact.
3948
+ responsibility; the decision surfaces as isBlocked (true) and blockReason (reason, or
3949
+ "Policy Violation" when omitted).
3950
+ Only BLOCK writes those two keys: saving or publishing a rule whose SET_FACT or
3951
+ MUTATE_FACT targets either one fails with ACT-033.
3952
+
3953
+ Evaluation order: every rule's condition is checked against the input facts before any
3954
+ action runs, so a condition never sees a value an action writes in the same request. Then
3955
+ the actions of the winning rules run in priority order. Each action works on the values
3956
+ the actions before it left, in its own rule or an earlier one. To test a value an action
3957
+ writes, send that value as an input fact in a second request. If a rule should run
3958
+ whenever another one does, put its actions in that rule instead.
3933
3959
 
3934
3960
  RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
3935
3961
  `,
@@ -4090,7 +4116,7 @@ function registerFactTools(server, callApi) {
4090
4116
  {
4091
4117
  title: "Update Fact Definition",
4092
4118
  annotations: { readOnlyHint: false, destructiveHint: true },
4093
- description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts accept name, description, and PII only.",
4119
+ description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts (isBlocked, blockReason) accept changes to name, description, PII, and the value domain; isRequired is ignored, and their type cannot change (FD-003).",
4094
4120
  inputSchema: z5.object({
4095
4121
  factId: z5.string().uuid().describe("Fact definition ID"),
4096
4122
  name: z5.string().optional().describe("Display name"),
@@ -4130,7 +4156,7 @@ function registerFactTools(server, callApi) {
4130
4156
  {
4131
4157
  title: "Get Action Runtime Fact Metadata",
4132
4158
  annotations: { readOnlyHint: true },
4133
- description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action that can write a key the request did not send, and its targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
4159
+ description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action whose targetVar the request does not have to send, and that targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
4134
4160
  inputSchema: z5.object({})
4135
4161
  },
4136
4162
  async () => callApi("GET", "schema/action-metadata")
@@ -5043,6 +5069,7 @@ function registerAllTools(server, callApi) {
5043
5069
  registerDomainTemplateTools(server, callApi);
5044
5070
  registerWebhookSubscriptionTools(server, callApi);
5045
5071
  }
5072
+ var SERVER_INFO = { name: "lexq", ...server_info_default };
5046
5073
 
5047
5074
  // src/mcp/server.ts
5048
5075
  var __dirname = dirname(fileURLToPath(import.meta.url));
@@ -5059,7 +5086,7 @@ function startMcpServer() {
5059
5086
  const callApi = createCallApiFromConfig();
5060
5087
  serveStdio(
5061
5088
  () => {
5062
- const server = new McpServer({ name: "lexq", version });
5089
+ const server = new McpServer({ ...SERVER_INFO, version });
5063
5090
  registerAllTools(server, callApi);
5064
5091
  return server;
5065
5092
  },
@@ -1,4 +1,4 @@
1
- import { McpServer } from '@modelcontextprotocol/server';
1
+ import { Implementation, McpServer } from '@modelcontextprotocol/server';
2
2
  import { LosslessNumber } from 'lossless-json';
3
3
  export { LosslessNumber } from 'lossless-json';
4
4
 
@@ -63,5 +63,14 @@ declare const isLosslessNumber: (value: unknown) => value is LosslessNumber;
63
63
  * @param callApi - API caller function (config-based for CLI, Bearer-based for HTTP)
64
64
  */
65
65
  declare function registerAllTools(server: McpServer, callApi: CallApi): void;
66
+ /**
67
+ * How the server introduces itself when a client connects: a title, a description and an
68
+ * icon the client can show, and a link to its website. A host serving these tools adds its
69
+ * own version.
70
+ *
71
+ * The values are kept in server-info.json rather than here because server.json lists them
72
+ * again for the MCP Registry, and `pnpm registry:check` has to read both without a build.
73
+ */
74
+ declare const SERVER_INFO: Omit<Implementation, 'version'>;
66
75
 
67
- export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
76
+ export { ApiError, type CallApi, type McpToolResult, SERVER_INFO, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
@@ -1,3 +1,17 @@
1
+ // src/mcp/server-info.json
2
+ var server_info_default = {
3
+ title: "LexQ",
4
+ description: "The Decision Operations Platform for engineering teams",
5
+ websiteUrl: "https://lexq.io",
6
+ icons: [
7
+ {
8
+ src: "https://lexq.io/logo.png",
9
+ mimeType: "image/png",
10
+ sizes: ["1024x1024"]
11
+ }
12
+ ]
13
+ };
14
+
1
15
  // src/mcp/tools/status.ts
2
16
  import { z } from "zod";
3
17
  function registerStatusTools(server, callApi) {
@@ -469,11 +483,21 @@ function registerRuleTools(server, callApi) {
469
483
  requires operand !== 0.
470
484
  - SET_FACT: { targetVar: string, value: string|number|boolean } targetVar must be a
471
485
  registered fact (ACT-032 otherwise). The request does not have to send it: SET_FACT is
472
- the only action that can write a key absent from the input facts. MUTATE_FACT requires
486
+ the only action whose targetVar may be absent from the input facts. MUTATE_FACT requires
473
487
  its target to be present in the input facts.
474
- - BLOCK: { reason: string } Records a rejection decision. It does NOT halt rule execution —
488
+ - BLOCK: { reason?: string } Records a rejection decision. It does NOT halt rule execution —
475
489
  subsequent actions and subsequent winning rules still run. Enforcement is the caller's
476
- responsibility; the decision surfaces as the isBlocked fact.
490
+ responsibility; the decision surfaces as isBlocked (true) and blockReason (reason, or
491
+ "Policy Violation" when omitted).
492
+ Only BLOCK writes those two keys: saving or publishing a rule whose SET_FACT or
493
+ MUTATE_FACT targets either one fails with ACT-033.
494
+
495
+ Evaluation order: every rule's condition is checked against the input facts before any
496
+ action runs, so a condition never sees a value an action writes in the same request. Then
497
+ the actions of the winning rules run in priority order. Each action works on the values
498
+ the actions before it left, in its own rule or an earlier one. To test a value an action
499
+ writes, send that value as an input fact in a second request. If a rule should run
500
+ whenever another one does, put its actions in that rule instead.
477
501
 
478
502
  RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..${MAX_ROUNDING_SCALE}), mode?: "HALF_UP"|"HALF_DOWN"|"HALF_EVEN"|"FLOOR"|"CEILING"|"DOWN"|"UP" } mode defaults to HALF_UP. When omitted, calculator output is preserved at full precision (lossless).
479
503
  `,
@@ -638,7 +662,7 @@ function registerFactTools(server, callApi) {
638
662
  {
639
663
  title: "Update Fact Definition",
640
664
  annotations: { readOnlyHint: false, destructiveHint: true },
641
- description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts accept name, description, and PII only.",
665
+ description: "Update a fact definition. The key is immutable. The type can change only while no rule references the fact; if any does, the call fails with FD-007 and reports the count. Rules in deleted groups do not count. Only the fields you send are changed. System facts (isBlocked, blockReason) accept changes to name, description, PII, and the value domain; isRequired is ignored, and their type cannot change (FD-003).",
642
666
  inputSchema: z5.object({
643
667
  factId: z5.string().uuid().describe("Fact definition ID"),
644
668
  name: z5.string().optional().describe("Display name"),
@@ -678,7 +702,7 @@ function registerFactTools(server, callApi) {
678
702
  {
679
703
  title: "Get Action Runtime Fact Metadata",
680
704
  annotations: { readOnlyHint: true },
681
- description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action that can write a key the request did not send, and its targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
705
+ description: "Retrieve runtime fact requirements per Action type. For each Action, shows which input facts must be present in the execution payload \u2014 e.g. MUTATE_FACT always requires its targetVar fact, plus refVar when one is specified. The factRequired flag describes the FACT, not the parameter: refVar is an optional parameter, but if you specify it the named fact must exist. A required fact absent at runtime throws \u2014 the engine never defaults to 0. These facts must be sent in the request (P-015 otherwise), even when an earlier action in the same rule writes them. SET_FACT requires no input fact: it is the only action whose targetVar the request does not have to send, and that targetVar must be a registered fact (ACT-032). Static data, safe to cache in-session.",
682
706
  inputSchema: z5.object({})
683
707
  },
684
708
  async () => callApi("GET", "schema/action-metadata")
@@ -1591,9 +1615,11 @@ function registerAllTools(server, callApi) {
1591
1615
  registerDomainTemplateTools(server, callApi);
1592
1616
  registerWebhookSubscriptionTools(server, callApi);
1593
1617
  }
1618
+ var SERVER_INFO = { name: "lexq", ...server_info_default };
1594
1619
  export {
1595
1620
  ApiError,
1596
1621
  LosslessNumber,
1622
+ SERVER_INFO,
1597
1623
  formatUnregisteredFactWarning,
1598
1624
  isLosslessNumber,
1599
1625
  paginationParams,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "mcpName": "io.lexq/lexq",
5
5
  "description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
6
6
  "type": "module",
@@ -182,7 +182,7 @@ Each rule can have multiple actions. Actions fire sequentially.
182
182
  |---------------|--------------------------------------------|----------------------------------------------------------------------|
183
183
  | `MUTATE_FACT` | Arithmetic change to a numeric fact | `targetVar`, `operator`, `method`, `operand`, `refVar?`, `rounding?` |
184
184
  | `SET_FACT` | Assign a literal value to a registered fact | `targetVar`, `value` |
185
- | `BLOCK` | Record a rejection decision | `reason` |
185
+ | `BLOCK` | Record a rejection decision | `reason?` |
186
186
 
187
187
  ### `MUTATE_FACT` parameters
188
188
 
@@ -247,9 +247,13 @@ request didn't send" is `SET_FACT`'s job.
247
247
 
248
248
  ### `BLOCK` does not halt execution
249
249
 
250
- `BLOCK` records a rejection decision by writing the `isBlocked` fact. Subsequent actions in the
251
- same rule and subsequent winning rules still run. Enforcement is the caller's responsibility —
252
- read `isBlocked` from the response.
250
+ `BLOCK` records a rejection decision by writing `isBlocked` (`true`) and `blockReason` (`reason`,
251
+ or `Policy Violation` when omitted). Subsequent actions in the same rule and subsequent winning
252
+ rules still run. Enforcement is the caller's responsibility — read `isBlocked` from the response.
253
+
254
+ Only `BLOCK` writes those two keys. Saving or publishing a rule whose `SET_FACT` or
255
+ `MUTATE_FACT` targets either one fails with `ACT-033`. No condition sees what `BLOCK` writes
256
+ in the same request: every condition is checked against the input facts before any action runs.
253
257
 
254
258
  ### Action Example: 10% Discount via MUTATE_FACT
255
259