@lexq/cli 0.1.45 → 0.1.47

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/dist/index.js CHANGED
@@ -48,6 +48,18 @@ function getConfigPath() {
48
48
  return CONFIG_FILE;
49
49
  }
50
50
 
51
+ // src/lib/lossless-json.ts
52
+ import {
53
+ LosslessNumber,
54
+ isSafeNumber,
55
+ parse as losslessParse,
56
+ stringify as losslessStringify
57
+ } from "lossless-json";
58
+ var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
59
+ var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
60
+ var parseJson = (text) => losslessParse(text, void 0, parseNumber);
61
+ var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
62
+
51
63
  // src/lib/api-client.ts
52
64
  var ApiError = class extends Error {
53
65
  constructor(statusCode, errorCode, message) {
@@ -82,7 +94,7 @@ async function doFetch(method, path, options) {
82
94
  console.log(` Content-Type: application/json`);
83
95
  if (options.body) {
84
96
  console.log("Body:");
85
- console.log(` ${JSON.stringify(options.body, null, 2)}`);
97
+ console.log(` ${stringifyJson(options.body, 2) ?? ""}`);
86
98
  }
87
99
  console.log("\n(Use without --dry-run to execute)");
88
100
  process.exit(0);
@@ -92,7 +104,7 @@ async function doFetch(method, path, options) {
92
104
  const response = await fetch(url.toString(), {
93
105
  method,
94
106
  headers,
95
- body: options.body ? JSON.stringify(options.body) : void 0
107
+ body: options.body ? stringifyJson(options.body) : void 0
96
108
  });
97
109
  if (options.verbose) {
98
110
  console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
@@ -121,7 +133,7 @@ async function apiRequestWithMeta(method, path, options = {}) {
121
133
  if (response.status === 204 || contentType === "") {
122
134
  return { data: void 0, meta: null };
123
135
  }
124
- const json = await response.json();
136
+ const json = parseJson(await response.text());
125
137
  assertOk(response, json);
126
138
  return { data: json.data, meta: json.meta ?? null };
127
139
  }
@@ -129,7 +141,7 @@ async function apiRequestWithMeta(method, path, options = {}) {
129
141
  // src/lib/output.ts
130
142
  import Table from "cli-table3";
131
143
  function printJson(data) {
132
- console.log(JSON.stringify(data, null, 2));
144
+ console.log(stringifyJson(data, 2));
133
145
  }
134
146
  function printTable(headers, rows, options) {
135
147
  const table = new Table({
@@ -976,7 +988,7 @@ ${data.length} total`);
976
988
  ).action(async (opts) => {
977
989
  try {
978
990
  const globalOpts = program.opts();
979
- const body = JSON.parse(opts.json);
991
+ const body = parseJson(opts.json);
980
992
  const { data, meta } = await apiRequestWithMeta(
981
993
  "POST",
982
994
  `policy-groups/${opts.groupId}/versions/${opts.versionId}/rules`,
@@ -1012,7 +1024,7 @@ ${data.length} total`);
1012
1024
  ).action(async (opts) => {
1013
1025
  try {
1014
1026
  const globalOpts = program.opts();
1015
- const body = JSON.parse(opts.json);
1027
+ const body = parseJson(opts.json);
1016
1028
  const { data, meta } = await apiRequestWithMeta(
1017
1029
  "PUT",
1018
1030
  `policy-groups/${opts.groupId}/versions/${opts.versionId}/rules/${opts.id}`,
@@ -1938,7 +1950,7 @@ function registerAnalyticsCommands(program) {
1938
1950
  ])
1939
1951
  );
1940
1952
  console.log("\nExample request:");
1941
- console.log(JSON.stringify(data.exampleRequest, null, 2));
1953
+ console.log(stringifyJson(data.exampleRequest, 2));
1942
1954
  } else {
1943
1955
  printJson(data);
1944
1956
  }
@@ -2186,7 +2198,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2186
2198
  }
2187
2199
  );
2188
2200
  if (opts.output) {
2189
- const text = typeof response === "string" ? response : JSON.stringify(response, null, 2);
2201
+ const text = typeof response === "string" ? response : stringifyJson(response, 2) ?? "";
2190
2202
  writeFileSync2(opts.output, text, "utf-8");
2191
2203
  console.log(`\u2713 Exported to ${opts.output}`);
2192
2204
  } else {
@@ -2309,10 +2321,10 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2309
2321
  function resolveBody(opts) {
2310
2322
  if (opts.file) {
2311
2323
  const raw = readFileSync2(opts.file, "utf-8");
2312
- return JSON.parse(raw);
2324
+ return parseJson(raw);
2313
2325
  }
2314
2326
  if (opts.json) {
2315
- return JSON.parse(opts.json);
2327
+ return parseJson(opts.json);
2316
2328
  }
2317
2329
  return {};
2318
2330
  }
@@ -3200,8 +3212,8 @@ function createCallApiFromConfig() {
3200
3212
  headers: { "X-API-KEY": config.apiKey },
3201
3213
  body: formData
3202
3214
  });
3203
- const data2 = await response.json();
3204
- return { content: [{ type: "text", text: JSON.stringify(data2, null, 2) }] };
3215
+ const data2 = parseJson(await response.text());
3216
+ return { content: [{ type: "text", text: stringifyJson(data2, 2) ?? "" }] };
3205
3217
  }
3206
3218
  const clientOpts = {
3207
3219
  apiKey: config.apiKey,
@@ -3213,7 +3225,7 @@ function createCallApiFromConfig() {
3213
3225
  params: opts?.params
3214
3226
  });
3215
3227
  const content = [
3216
- { type: "text", text: JSON.stringify(data, null, 2) }
3228
+ { type: "text", text: stringifyJson(data, 2) ?? "" }
3217
3229
  ];
3218
3230
  const warning = formatUnregisteredFactWarning(meta);
3219
3231
  if (warning) content.push({ type: "text", text: warning });
@@ -3256,7 +3268,7 @@ function registerStatusTools(server, callApi) {
3256
3268
  "lexq_whoami",
3257
3269
  {
3258
3270
  title: "Who Am I",
3259
- description: "Show current authentication info (tenant name, role, API key mask).",
3271
+ description: "Show current authentication info (tenant ID, user ID, role).",
3260
3272
  inputSchema: {}
3261
3273
  },
3262
3274
  async () => callApi("GET", "whoami")
@@ -3577,7 +3589,7 @@ function registerRuleTools(server, callApi) {
3577
3589
  subsequent actions and subsequent winning rules still run. Enforcement is the caller's
3578
3590
  responsibility; the decision surfaces as the is_blocked fact.
3579
3591
 
3580
- RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..16), 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).
3592
+ RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..34), 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).
3581
3593
  `,
3582
3594
  inputSchema: {
3583
3595
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -3588,7 +3600,7 @@ function registerRuleTools(server, callApi) {
3588
3600
  }
3589
3601
  },
3590
3602
  async ({ groupId, versionId, rule }) => {
3591
- const body = JSON.parse(rule);
3603
+ const body = parseJson(rule);
3592
3604
  return callApi("POST", `policy-groups/${groupId}/versions/${versionId}/rules`, { body });
3593
3605
  }
3594
3606
  );
@@ -3607,7 +3619,7 @@ function registerRuleTools(server, callApi) {
3607
3619
  }
3608
3620
  },
3609
3621
  async ({ groupId, versionId, ruleId, rule }) => {
3610
- const body = JSON.parse(rule);
3622
+ const body = parseJson(rule);
3611
3623
  return callApi("PUT", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
3612
3624
  body
3613
3625
  });
@@ -3953,7 +3965,7 @@ function registerAnalyticsTools(server, callApi) {
3953
3965
  }
3954
3966
  },
3955
3967
  async ({ versionId, facts, includeDebugInfo }) => {
3956
- const parsedFacts = JSON.parse(facts);
3968
+ const parsedFacts = parseJson(facts);
3957
3969
  return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
3958
3970
  body: { facts: parsedFacts, includeDebugInfo }
3959
3971
  });
@@ -3977,7 +3989,7 @@ function registerAnalyticsTools(server, callApi) {
3977
3989
  }
3978
3990
  },
3979
3991
  async ({ versionIdA, versionIdB, facts }) => {
3980
- const parsedFacts = JSON.parse(facts);
3992
+ const parsedFacts = parseJson(facts);
3981
3993
  return callApi("POST", "analytics/dry-run/compare", {
3982
3994
  body: { versionIdA, versionIdB, facts: parsedFacts }
3983
3995
  });
@@ -4032,7 +4044,7 @@ function registerAnalyticsTools(server, callApi) {
4032
4044
  }
4033
4045
  },
4034
4046
  async ({ body }) => {
4035
- const parsed = JSON.parse(body);
4047
+ const parsed = parseJson(body);
4036
4048
  return callApi("POST", "analytics/simulations", { body: parsed });
4037
4049
  }
4038
4050
  );
@@ -4122,12 +4134,12 @@ function registerAnalyticsTools(server, callApi) {
4122
4134
  });
4123
4135
  if (!result.isError) {
4124
4136
  try {
4125
- const uploaded = JSON.parse(result.content[0]?.text ?? "{}");
4137
+ const uploaded = parseJson(result.content[0]?.text ?? "{}");
4126
4138
  if (uploaded.path) {
4127
4139
  const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
4128
4140
  result.content.push({
4129
4141
  type: "text",
4130
- text: "Ready-to-use dataset block for lexq_simulation_start:\n" + JSON.stringify({ dataset }, null, 2)
4142
+ text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
4131
4143
  });
4132
4144
  }
4133
4145
  } catch {
@@ -1,4 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { LosslessNumber } from 'lossless-json';
3
+ export { LosslessNumber } from 'lossless-json';
2
4
 
3
5
  interface McpToolResult {
4
6
  [key: string]: unknown;
@@ -41,6 +43,19 @@ declare class ApiError extends Error {
41
43
  constructor(statusCode: number, errorCode: string | null, message: string);
42
44
  }
43
45
 
46
+ /** Parses JSON text without rounding numbers that a double cannot hold. */
47
+ declare const parseJson: (text: string) => unknown;
48
+ /**
49
+ * Serializes to JSON, writing preserved numbers back as plain number literals.
50
+ *
51
+ * Use this rather than `JSON.stringify` anywhere a value may have come from {@link parseJson}.
52
+ * The native one turns a preserved number into an object, which breaks both the wire format and
53
+ * `lexq ... --json | jq`.
54
+ */
55
+ declare const stringifyJson: (value: unknown, space?: number | string) => string | undefined;
56
+ /** Type guard for a preserved number. Check this before any generic object branch. */
57
+ declare const isLosslessNumber: (value: unknown) => value is LosslessNumber;
58
+
44
59
  /**
45
60
  * Registers all MCP tools on the given server.
46
61
  *
@@ -49,4 +64,4 @@ declare class ApiError extends Error {
49
64
  */
50
65
  declare function registerAllTools(server: McpServer, callApi: CallApi): void;
51
66
 
52
- export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, paginationParams, registerAllTools };
67
+ export { ApiError, type CallApi, type McpToolResult, formatUnregisteredFactWarning, isLosslessNumber, paginationParams, parseJson, registerAllTools, stringifyJson };
@@ -4,7 +4,7 @@ function registerStatusTools(server, callApi) {
4
4
  "lexq_whoami",
5
5
  {
6
6
  title: "Who Am I",
7
- description: "Show current authentication info (tenant name, role, API key mask).",
7
+ description: "Show current authentication info (tenant ID, user ID, role).",
8
8
  inputSchema: {}
9
9
  },
10
10
  async () => callApi("GET", "whoami")
@@ -172,6 +172,19 @@ import { join } from "path";
172
172
  var CONFIG_DIR = join(homedir(), ".lexq");
173
173
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
174
174
 
175
+ // src/lib/lossless-json.ts
176
+ import {
177
+ LosslessNumber,
178
+ isSafeNumber,
179
+ parse as losslessParse,
180
+ stringify as losslessStringify
181
+ } from "lossless-json";
182
+ var survivesDoubleRoundTrip = (literal) => isSafeNumber(literal);
183
+ var parseNumber = (value) => survivesDoubleRoundTrip(value) ? parseFloat(value) : new LosslessNumber(value);
184
+ var parseJson = (text) => losslessParse(text, void 0, parseNumber);
185
+ var stringifyJson = (value, space) => losslessStringify(value, void 0, space);
186
+ var isLosslessNumber = (value) => value instanceof LosslessNumber;
187
+
175
188
  // src/lib/api-client.ts
176
189
  var ApiError = class extends Error {
177
190
  constructor(statusCode, errorCode, message) {
@@ -388,7 +401,7 @@ function registerRuleTools(server, callApi) {
388
401
  subsequent actions and subsequent winning rules still run. Enforcement is the caller's
389
402
  responsibility; the decision surfaces as the is_blocked fact.
390
403
 
391
- RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..16), 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).
404
+ RoundingOption (optional, MUTATE_FACT only): { scale: integer (0..34), 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).
392
405
  `,
393
406
  inputSchema: {
394
407
  groupId: z3.string().uuid().describe("Policy group ID"),
@@ -399,7 +412,7 @@ function registerRuleTools(server, callApi) {
399
412
  }
400
413
  },
401
414
  async ({ groupId, versionId, rule }) => {
402
- const body = JSON.parse(rule);
415
+ const body = parseJson(rule);
403
416
  return callApi("POST", `policy-groups/${groupId}/versions/${versionId}/rules`, { body });
404
417
  }
405
418
  );
@@ -418,7 +431,7 @@ function registerRuleTools(server, callApi) {
418
431
  }
419
432
  },
420
433
  async ({ groupId, versionId, ruleId, rule }) => {
421
- const body = JSON.parse(rule);
434
+ const body = parseJson(rule);
422
435
  return callApi("PUT", `policy-groups/${groupId}/versions/${versionId}/rules/${ruleId}`, {
423
436
  body
424
437
  });
@@ -764,7 +777,7 @@ function registerAnalyticsTools(server, callApi) {
764
777
  }
765
778
  },
766
779
  async ({ versionId, facts, includeDebugInfo }) => {
767
- const parsedFacts = JSON.parse(facts);
780
+ const parsedFacts = parseJson(facts);
768
781
  return callApi("POST", `analytics/dry-run/versions/${versionId}`, {
769
782
  body: { facts: parsedFacts, includeDebugInfo }
770
783
  });
@@ -788,7 +801,7 @@ function registerAnalyticsTools(server, callApi) {
788
801
  }
789
802
  },
790
803
  async ({ versionIdA, versionIdB, facts }) => {
791
- const parsedFacts = JSON.parse(facts);
804
+ const parsedFacts = parseJson(facts);
792
805
  return callApi("POST", "analytics/dry-run/compare", {
793
806
  body: { versionIdA, versionIdB, facts: parsedFacts }
794
807
  });
@@ -843,7 +856,7 @@ function registerAnalyticsTools(server, callApi) {
843
856
  }
844
857
  },
845
858
  async ({ body }) => {
846
- const parsed = JSON.parse(body);
859
+ const parsed = parseJson(body);
847
860
  return callApi("POST", "analytics/simulations", { body: parsed });
848
861
  }
849
862
  );
@@ -933,12 +946,12 @@ function registerAnalyticsTools(server, callApi) {
933
946
  });
934
947
  if (!result.isError) {
935
948
  try {
936
- const uploaded = JSON.parse(result.content[0]?.text ?? "{}");
949
+ const uploaded = parseJson(result.content[0]?.text ?? "{}");
937
950
  if (uploaded.path) {
938
951
  const dataset = { type: "UPLOADED", source: "S3_BUCKET", path: uploaded.path };
939
952
  result.content.push({
940
953
  type: "text",
941
- text: "Ready-to-use dataset block for lexq_simulation_start:\n" + JSON.stringify({ dataset }, null, 2)
954
+ text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
942
955
  });
943
956
  }
944
957
  } catch {
@@ -1383,7 +1396,11 @@ function registerAllTools(server, callApi) {
1383
1396
  }
1384
1397
  export {
1385
1398
  ApiError,
1399
+ LosslessNumber,
1386
1400
  formatUnregisteredFactWarning,
1401
+ isLosslessNumber,
1387
1402
  paginationParams,
1388
- registerAllTools
1403
+ parseJson,
1404
+ registerAllTools,
1405
+ stringifyJson
1389
1406
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "description": "LexQ CLI — manage policies, simulate rules, and deploy from the terminal. Built for humans and AI agents.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,7 @@
61
61
  "cli-table3": "^0.6.5",
62
62
  "commander": "^13.1.0",
63
63
  "dedent": "^1.7.2",
64
+ "lossless-json": "4.3.1",
64
65
  "zod": "^3.25.76"
65
66
  },
66
67
  "devDependencies": {
@@ -193,7 +193,7 @@ Each rule can have multiple actions. Actions fire sequentially.
193
193
  | `method` | always | `PERCENTAGE` \| `AMOUNT` — dictates the unit of `operand` |
194
194
  | `operand` | always | The arithmetic operand. Percent when PERCENTAGE, absolute amount when AMOUNT. |
195
195
  | `refVar` | optional | Base for percentage calculation. Omit to use `targetVar` itself. |
196
- | `rounding` | optional | `{ scale: 0..16, mode?: HALF_UP \| ... }`. Omit for lossless full precision. |
196
+ | `rounding` | optional | `{ scale: 0..34, mode?: HALF_UP \| ... }`. Omit for lossless full precision. |
197
197
 
198
198
  **operator × method matrix**
199
199