@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 +34 -22
- package/dist/mcp/register.d.ts +16 -1
- package/dist/mcp/register.js +27 -10
- package/package.json +2 -1
- package/skills/lexq-rules/SKILL.md +1 -1
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(` ${
|
|
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 ?
|
|
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.
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 :
|
|
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
|
|
2324
|
+
return parseJson(raw);
|
|
2313
2325
|
}
|
|
2314
2326
|
if (opts.json) {
|
|
2315
|
-
return
|
|
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.
|
|
3204
|
-
return { content: [{ type: "text", text:
|
|
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:
|
|
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
|
|
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..
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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" +
|
|
4142
|
+
text: "Ready-to-use dataset block for lexq_simulation_start:\n" + stringifyJson({ dataset }, 2)
|
|
4131
4143
|
});
|
|
4132
4144
|
}
|
|
4133
4145
|
} catch {
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -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 };
|
package/dist/mcp/register.js
CHANGED
|
@@ -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
|
|
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..
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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" +
|
|
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
|
-
|
|
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.
|
|
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..
|
|
196
|
+
| `rounding` | optional | `{ scale: 0..34, mode?: HALF_UP \| ... }`. Omit for lossless full precision. |
|
|
197
197
|
|
|
198
198
|
**operator × method matrix**
|
|
199
199
|
|