@lexq/cli 0.1.53 → 0.1.55
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 +5 -4
- package/dist/index.js +141 -25
- package/dist/mcp/register.js +34 -3
- package/package.json +1 -1
- package/skills/lexq-execution/SKILL.md +8 -0
- package/skills/lexq-shared/SKILL.md +1 -1
- package/skills/lexq-simulation/SKILL.md +3 -3
package/AGENTS.md
CHANGED
|
@@ -61,7 +61,7 @@ lexq groups list|get|create|update|delete|reorder
|
|
|
61
61
|
lexq groups ab-test start|stop|adjust
|
|
62
62
|
lexq versions list|get|create|update|delete|clone
|
|
63
63
|
lexq rules list|get|create|update|delete|reorder|toggle
|
|
64
|
-
lexq facts list|create|update|delete|action-metadata|unregistered
|
|
64
|
+
lexq facts list|create|update|delete|export|action-metadata|unregistered
|
|
65
65
|
lexq domain-templates list|preview|apply
|
|
66
66
|
lexq deploy publish|live|rollback|undeploy|history|detail|overview|deployable|diff|schedule|unschedule|schedules
|
|
67
67
|
lexq analytics dry-run|dry-run-compare|requirements
|
|
@@ -69,7 +69,7 @@ lexq analytics simulation start|status|list|cancel|export
|
|
|
69
69
|
lexq analytics dataset upload|template
|
|
70
70
|
lexq profile <groupId>
|
|
71
71
|
lexq history list|get|stats
|
|
72
|
-
lexq replay decision|start|list|get|cancel
|
|
72
|
+
lexq replay decision|start|list|get|cancel|export
|
|
73
73
|
lexq provenance get|reveal-audits
|
|
74
74
|
lexq logs list|get|action|bulk-action
|
|
75
75
|
lexq webhook-subscriptions list|get|save|delete|test
|
|
@@ -129,8 +129,9 @@ Place this file in the project root. The IDE will auto-discover it.
|
|
|
129
129
|
|
|
130
130
|
### Claude Code
|
|
131
131
|
|
|
132
|
-
Reads this file
|
|
133
|
-
guide.
|
|
132
|
+
Reads this file through the root `CLAUDE.md`, which is a one-line import rather than a copy. There is no
|
|
133
|
+
Claude-specific document to keep in sync. The CLI guide is this file plus `skills/`; `CONTEXT.md` covers platform
|
|
134
|
+
architecture separately and ships alongside.
|
|
134
135
|
|
|
135
136
|
### Gemini CLI
|
|
136
137
|
|
package/dist/index.js
CHANGED
|
@@ -111,6 +111,12 @@ async function doFetch(method, path, options) {
|
|
|
111
111
|
}
|
|
112
112
|
return response;
|
|
113
113
|
}
|
|
114
|
+
function isFileResponse(response) {
|
|
115
|
+
const disposition = response.headers.get("content-disposition") ?? "";
|
|
116
|
+
if (disposition.toLowerCase().includes("attachment")) return true;
|
|
117
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
+
return contentType.includes("text/csv") || contentType.includes("application/octet-stream");
|
|
119
|
+
}
|
|
114
120
|
function assertOk(response, json) {
|
|
115
121
|
if (!response.ok || json.result !== "SUCCESS") {
|
|
116
122
|
throw new ApiError(
|
|
@@ -126,10 +132,10 @@ async function apiRequest(method, path, options = {}) {
|
|
|
126
132
|
}
|
|
127
133
|
async function apiRequestWithMeta(method, path, options = {}) {
|
|
128
134
|
const response = await doFetch(method, path, options);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return { data: response, meta: null };
|
|
135
|
+
if (isFileResponse(response)) {
|
|
136
|
+
return { data: await response.text(), meta: null };
|
|
132
137
|
}
|
|
138
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
133
139
|
if (response.status === 204 || contentType === "") {
|
|
134
140
|
return { data: void 0, meta: null };
|
|
135
141
|
}
|
|
@@ -1164,6 +1170,33 @@ ${data.length} total`);
|
|
|
1164
1170
|
// src/commands/facts.ts
|
|
1165
1171
|
import "commander";
|
|
1166
1172
|
import dedent6 from "dedent";
|
|
1173
|
+
|
|
1174
|
+
// src/lib/export.ts
|
|
1175
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
1176
|
+
function parseFormat(value) {
|
|
1177
|
+
if (value === "csv" || value === "json") return value;
|
|
1178
|
+
throw new Error(`Unsupported export format: ${String(value)}. Use csv or json.`);
|
|
1179
|
+
}
|
|
1180
|
+
async function runExport(path, format, options) {
|
|
1181
|
+
const { output, params, ...clientOptions } = options;
|
|
1182
|
+
const body = await apiRequest("GET", path, {
|
|
1183
|
+
...clientOptions,
|
|
1184
|
+
params: { ...params, format }
|
|
1185
|
+
});
|
|
1186
|
+
if (typeof body !== "string") {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Export did not return a file (${path}). Re-run with --verbose to see the response headers.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
if (output) {
|
|
1192
|
+
writeFileSync2(output, body, "utf-8");
|
|
1193
|
+
console.log(`\u2713 Exported to ${output}`);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
process.stdout.write(body.endsWith("\n") ? body : body + "\n");
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// src/commands/facts.ts
|
|
1167
1200
|
function registerFactCommands(program) {
|
|
1168
1201
|
const facts = program.command("facts").description("Manage fact definitions (schema)").addHelpText(
|
|
1169
1202
|
"after",
|
|
@@ -1360,6 +1393,36 @@ ${data.length} unregistered`);
|
|
|
1360
1393
|
process.exit(1);
|
|
1361
1394
|
}
|
|
1362
1395
|
});
|
|
1396
|
+
facts.command("export").description("Export the fact catalog").option("--as <fmt>", "Exported file format: csv or json", "csv").option("--keyword <keyword>", "Filter by key or name").option("--output <path>", "Output file path").addHelpText(
|
|
1397
|
+
"after",
|
|
1398
|
+
dedent6`
|
|
1399
|
+
|
|
1400
|
+
The two formats carry different things. CSV is the catalog as it stands, system
|
|
1401
|
+
facts included, for reading in a spreadsheet. JSON matches the shape that
|
|
1402
|
+
batch create accepts, so it can be fed straight back in — which is why it leaves
|
|
1403
|
+
out the fields that endpoint does not take.
|
|
1404
|
+
|
|
1405
|
+
Examples:
|
|
1406
|
+
$ lexq facts export --output facts.csv
|
|
1407
|
+
$ lexq facts export --as json --output facts.json
|
|
1408
|
+
$ lexq facts export --keyword user --as json
|
|
1409
|
+
`
|
|
1410
|
+
).action(async (opts) => {
|
|
1411
|
+
try {
|
|
1412
|
+
const globalOpts = program.opts();
|
|
1413
|
+
await runExport("schema/facts/export", parseFormat(opts.as), {
|
|
1414
|
+
apiKey: globalOpts.apiKey,
|
|
1415
|
+
baseUrl: globalOpts.baseUrl,
|
|
1416
|
+
dryRun: globalOpts.dryRun,
|
|
1417
|
+
verbose: globalOpts.verbose,
|
|
1418
|
+
output: opts.output,
|
|
1419
|
+
params: opts.keyword ? { keyword: opts.keyword } : void 0
|
|
1420
|
+
});
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
printError(error);
|
|
1423
|
+
process.exit(1);
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1363
1426
|
facts.command("action-metadata").description("Get action runtime fact metadata").addHelpText(
|
|
1364
1427
|
"after",
|
|
1365
1428
|
dedent6`
|
|
@@ -1843,7 +1906,7 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
|
1843
1906
|
}
|
|
1844
1907
|
|
|
1845
1908
|
// src/commands/analytics.ts
|
|
1846
|
-
import { readFileSync as readFileSync2, writeFileSync as
|
|
1909
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1847
1910
|
import "commander";
|
|
1848
1911
|
import dedent8 from "dedent";
|
|
1849
1912
|
function registerAnalyticsCommands(program) {
|
|
@@ -2191,38 +2254,30 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2191
2254
|
process.exit(1);
|
|
2192
2255
|
}
|
|
2193
2256
|
});
|
|
2194
|
-
sim.command("export").description("Export simulation results").requiredOption("--id <simulationId>", "Simulation ID").option("--
|
|
2257
|
+
sim.command("export").description("Export simulation results").requiredOption("--id <simulationId>", "Simulation ID").option("--as <fmt>", "Exported file format: csv or json", "json").option("--output <path>", "Output file path").addHelpText(
|
|
2195
2258
|
"after",
|
|
2196
2259
|
dedent8`
|
|
2197
2260
|
|
|
2198
2261
|
Only COMPLETED simulations can be exported.
|
|
2199
2262
|
|
|
2200
2263
|
Examples:
|
|
2201
|
-
$ lexq analytics simulation export --id <simId> --
|
|
2202
|
-
$ lexq analytics simulation export --id <simId> --
|
|
2264
|
+
$ lexq analytics simulation export --id <simId> --as csv --output results.csv
|
|
2265
|
+
$ lexq analytics simulation export --id <simId> --as json
|
|
2203
2266
|
`
|
|
2204
2267
|
).action(async (opts) => {
|
|
2205
2268
|
try {
|
|
2206
2269
|
const globalOpts = program.opts();
|
|
2207
|
-
|
|
2208
|
-
const response = await apiRequest(
|
|
2209
|
-
"GET",
|
|
2270
|
+
await runExport(
|
|
2210
2271
|
`analytics/simulations/${opts.id}/export`,
|
|
2272
|
+
parseFormat(opts.as),
|
|
2211
2273
|
{
|
|
2212
2274
|
apiKey: globalOpts.apiKey,
|
|
2213
2275
|
baseUrl: globalOpts.baseUrl,
|
|
2214
2276
|
dryRun: globalOpts.dryRun,
|
|
2215
2277
|
verbose: globalOpts.verbose,
|
|
2216
|
-
|
|
2278
|
+
output: opts.output
|
|
2217
2279
|
}
|
|
2218
2280
|
);
|
|
2219
|
-
if (opts.output) {
|
|
2220
|
-
const text = typeof response === "string" ? response : stringifyJson(response, 2) ?? "";
|
|
2221
|
-
writeFileSync2(opts.output, text, "utf-8");
|
|
2222
|
-
console.log(`\u2713 Exported to ${opts.output}`);
|
|
2223
|
-
} else {
|
|
2224
|
-
printJson(response);
|
|
2225
|
-
}
|
|
2226
2281
|
} catch (error) {
|
|
2227
2282
|
printError(error);
|
|
2228
2283
|
process.exit(1);
|
|
@@ -2293,7 +2348,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2293
2348
|
process.exit(1);
|
|
2294
2349
|
}
|
|
2295
2350
|
});
|
|
2296
|
-
dataset.command("template").description("Download a dataset template based on version requirements").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").option("--
|
|
2351
|
+
dataset.command("template").description("Download a dataset template based on version requirements").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").option("--as <fmt>", "Template file format: csv or json", "csv").option("--output <path>", "Output file path").addHelpText(
|
|
2297
2352
|
"after",
|
|
2298
2353
|
dedent8`
|
|
2299
2354
|
|
|
@@ -2301,7 +2356,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2301
2356
|
|
|
2302
2357
|
Examples:
|
|
2303
2358
|
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
|
|
2304
|
-
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --
|
|
2359
|
+
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --as json
|
|
2305
2360
|
`
|
|
2306
2361
|
).action(async (opts) => {
|
|
2307
2362
|
try {
|
|
@@ -2312,7 +2367,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2312
2367
|
if (!apiKey) {
|
|
2313
2368
|
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2314
2369
|
}
|
|
2315
|
-
const fmt = opts.
|
|
2370
|
+
const fmt = parseFormat(opts.as);
|
|
2316
2371
|
const url = new URL(
|
|
2317
2372
|
`analytics/groups/${opts.groupId}/versions/${opts.versionId}/dataset-template`,
|
|
2318
2373
|
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
@@ -2326,7 +2381,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2326
2381
|
}
|
|
2327
2382
|
const text = await response.text();
|
|
2328
2383
|
if (opts.output) {
|
|
2329
|
-
|
|
2384
|
+
writeFileSync3(opts.output, text, "utf-8");
|
|
2330
2385
|
console.log(`\u2713 Template saved to ${opts.output}`);
|
|
2331
2386
|
} else {
|
|
2332
2387
|
console.log(text);
|
|
@@ -2359,6 +2414,7 @@ import "commander";
|
|
|
2359
2414
|
import dedent9 from "dedent";
|
|
2360
2415
|
|
|
2361
2416
|
// src/types/enums.ts
|
|
2417
|
+
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
2362
2418
|
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
2363
2419
|
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
2364
2420
|
var ProfileCacheState = ["HIT", "MISS"];
|
|
@@ -2765,6 +2821,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2765
2821
|
process.exit(1);
|
|
2766
2822
|
}
|
|
2767
2823
|
});
|
|
2824
|
+
replay.command("export").description("Export the result of a completed replay job").requiredOption("--id <jobId>", "Replay job ID").option("--as <fmt>", "Exported file format: csv or json", "json").option("--output <path>", "Output file path").addHelpText(
|
|
2825
|
+
"after",
|
|
2826
|
+
dedent11`
|
|
2827
|
+
|
|
2828
|
+
Only COMPLETED jobs can be exported. A running job has no full result yet, and a
|
|
2829
|
+
partial one reads as the whole thing on the receiving end.
|
|
2830
|
+
|
|
2831
|
+
The two formats carry different things. CSV holds the effect blast radius, whose
|
|
2832
|
+
columns are fixed. JSON holds the changed samples and action parameters, which
|
|
2833
|
+
nest and whose keys differ per tenant. JSON is not a superset of CSV.
|
|
2834
|
+
|
|
2835
|
+
Examples:
|
|
2836
|
+
$ lexq replay export --id <jobId> --as csv --output blast-radius.csv
|
|
2837
|
+
$ lexq replay export --id <jobId> --as json
|
|
2838
|
+
`
|
|
2839
|
+
).action(async (opts) => {
|
|
2840
|
+
try {
|
|
2841
|
+
const globalOpts = program.opts();
|
|
2842
|
+
await runExport(`replay/jobs/${opts.id}/export`, parseFormat(opts.as), {
|
|
2843
|
+
apiKey: globalOpts.apiKey,
|
|
2844
|
+
baseUrl: globalOpts.baseUrl,
|
|
2845
|
+
dryRun: globalOpts.dryRun,
|
|
2846
|
+
verbose: globalOpts.verbose,
|
|
2847
|
+
output: opts.output
|
|
2848
|
+
});
|
|
2849
|
+
} catch (error) {
|
|
2850
|
+
printError(error);
|
|
2851
|
+
process.exit(1);
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2768
2854
|
}
|
|
2769
2855
|
|
|
2770
2856
|
// src/commands/provenance.ts
|
|
@@ -3722,6 +3808,7 @@ import { z as z4 } from "zod";
|
|
|
3722
3808
|
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
3723
3809
|
|
|
3724
3810
|
// src/mcp/tools/facts.ts
|
|
3811
|
+
var VALUE_TYPES = ValueType;
|
|
3725
3812
|
function registerFactTools(server, callApi) {
|
|
3726
3813
|
server.registerTool(
|
|
3727
3814
|
"lexq_facts_list",
|
|
@@ -3748,7 +3835,7 @@ function registerFactTools(server, callApi) {
|
|
|
3748
3835
|
inputSchema: {
|
|
3749
3836
|
key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
3750
3837
|
name: z4.string().describe("Display name"),
|
|
3751
|
-
type: z4.enum(
|
|
3838
|
+
type: z4.enum(VALUE_TYPES).describe("Value type"),
|
|
3752
3839
|
description: z4.string().optional().describe("Description"),
|
|
3753
3840
|
isRequired: z4.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
|
|
3754
3841
|
isPii: z4.boolean().default(false).describe(
|
|
@@ -3762,11 +3849,14 @@ function registerFactTools(server, callApi) {
|
|
|
3762
3849
|
"lexq_facts_update",
|
|
3763
3850
|
{
|
|
3764
3851
|
title: "Update Fact Definition",
|
|
3765
|
-
description: "Update a fact definition.
|
|
3852
|
+
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. Only the fields you send are changed. System facts accept name, description, and PII only.",
|
|
3766
3853
|
inputSchema: {
|
|
3767
3854
|
factId: z4.string().uuid().describe("Fact definition ID"),
|
|
3768
3855
|
name: z4.string().optional().describe("Display name"),
|
|
3769
3856
|
description: z4.string().optional().describe("Description"),
|
|
3857
|
+
type: z4.enum(VALUE_TYPES).optional().describe(
|
|
3858
|
+
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
|
|
3859
|
+
),
|
|
3770
3860
|
isRequired: z4.boolean().optional().describe("Required flag"),
|
|
3771
3861
|
isPii: z4.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)")
|
|
3772
3862
|
}
|
|
@@ -3777,7 +3867,7 @@ function registerFactTools(server, callApi) {
|
|
|
3777
3867
|
"lexq_facts_delete",
|
|
3778
3868
|
{
|
|
3779
3869
|
title: "Delete Fact Definition",
|
|
3780
|
-
description: "Delete a fact definition. System facts cannot be deleted.",
|
|
3870
|
+
description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Remove the references first.",
|
|
3781
3871
|
inputSchema: {
|
|
3782
3872
|
factId: z4.string().uuid().describe("Fact definition ID")
|
|
3783
3873
|
}
|
|
@@ -3805,6 +3895,20 @@ function registerFactTools(server, callApi) {
|
|
|
3805
3895
|
},
|
|
3806
3896
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
3807
3897
|
);
|
|
3898
|
+
server.registerTool(
|
|
3899
|
+
"lexq_facts_export",
|
|
3900
|
+
{
|
|
3901
|
+
title: "Export Fact Catalog",
|
|
3902
|
+
description: "Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet; JSON matches the shape that batch create accepts, so it can be fed straight back in, which is why it leaves out the fields that endpoint does not take. Returns the file contents as text.",
|
|
3903
|
+
inputSchema: {
|
|
3904
|
+
format: z4.enum(["csv", "json"]).default("csv").describe("Export format"),
|
|
3905
|
+
keyword: z4.string().optional().describe("Filter by key or name")
|
|
3906
|
+
}
|
|
3907
|
+
},
|
|
3908
|
+
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
3909
|
+
params: keyword ? { format, keyword } : { format }
|
|
3910
|
+
})
|
|
3911
|
+
);
|
|
3808
3912
|
}
|
|
3809
3913
|
|
|
3810
3914
|
// src/mcp/tools/deploy.ts
|
|
@@ -4318,6 +4422,18 @@ function registerReplayTools(server, callApi) {
|
|
|
4318
4422
|
},
|
|
4319
4423
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
4320
4424
|
);
|
|
4425
|
+
server.registerTool(
|
|
4426
|
+
"lexq_replay_export",
|
|
4427
|
+
{
|
|
4428
|
+
title: "Export Replay Result",
|
|
4429
|
+
description: "Export a COMPLETED window replay job. A running job is rejected \u2014 a partial result reads as the whole thing on the receiving end. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text.",
|
|
4430
|
+
inputSchema: {
|
|
4431
|
+
jobId: z8.string().describe("Replay job ID"),
|
|
4432
|
+
format: z8.enum(["json", "csv"]).default("json").describe("Export format")
|
|
4433
|
+
}
|
|
4434
|
+
},
|
|
4435
|
+
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
4436
|
+
);
|
|
4321
4437
|
}
|
|
4322
4438
|
|
|
4323
4439
|
// src/mcp/tools/history.ts
|
package/dist/mcp/register.js
CHANGED
|
@@ -15,6 +15,7 @@ function registerStatusTools(server, callApi) {
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
|
|
17
17
|
// src/types/enums.ts
|
|
18
|
+
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
18
19
|
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
19
20
|
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
20
21
|
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
@@ -514,6 +515,7 @@ import { z as z4 } from "zod";
|
|
|
514
515
|
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
515
516
|
|
|
516
517
|
// src/mcp/tools/facts.ts
|
|
518
|
+
var VALUE_TYPES = ValueType;
|
|
517
519
|
function registerFactTools(server, callApi) {
|
|
518
520
|
server.registerTool(
|
|
519
521
|
"lexq_facts_list",
|
|
@@ -540,7 +542,7 @@ function registerFactTools(server, callApi) {
|
|
|
540
542
|
inputSchema: {
|
|
541
543
|
key: z4.string().regex(FACT_KEY_PATTERN).describe("Variable key. Any casing; must start with a letter."),
|
|
542
544
|
name: z4.string().describe("Display name"),
|
|
543
|
-
type: z4.enum(
|
|
545
|
+
type: z4.enum(VALUE_TYPES).describe("Value type"),
|
|
544
546
|
description: z4.string().optional().describe("Description"),
|
|
545
547
|
isRequired: z4.boolean().default(false).describe("Whether this fact is required for rule evaluation"),
|
|
546
548
|
isPii: z4.boolean().default(false).describe(
|
|
@@ -554,11 +556,14 @@ function registerFactTools(server, callApi) {
|
|
|
554
556
|
"lexq_facts_update",
|
|
555
557
|
{
|
|
556
558
|
title: "Update Fact Definition",
|
|
557
|
-
description: "Update a fact definition.
|
|
559
|
+
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. Only the fields you send are changed. System facts accept name, description, and PII only.",
|
|
558
560
|
inputSchema: {
|
|
559
561
|
factId: z4.string().uuid().describe("Fact definition ID"),
|
|
560
562
|
name: z4.string().optional().describe("Display name"),
|
|
561
563
|
description: z4.string().optional().describe("Description"),
|
|
564
|
+
type: z4.enum(VALUE_TYPES).optional().describe(
|
|
565
|
+
"Value type. Omit to leave it unchanged. Changing it fails with FD-007 while any rule references the fact."
|
|
566
|
+
),
|
|
562
567
|
isRequired: z4.boolean().optional().describe("Required flag"),
|
|
563
568
|
isPii: z4.boolean().optional().describe("PII flag \u2014 enables/disables masking (changeable even on system facts)")
|
|
564
569
|
}
|
|
@@ -569,7 +574,7 @@ function registerFactTools(server, callApi) {
|
|
|
569
574
|
"lexq_facts_delete",
|
|
570
575
|
{
|
|
571
576
|
title: "Delete Fact Definition",
|
|
572
|
-
description: "Delete a fact definition. System facts cannot be deleted.",
|
|
577
|
+
description: "Delete a fact definition. System facts cannot be deleted. Neither can a fact that any rule references: that call fails with FD-006 and reports the count. Remove the references first.",
|
|
573
578
|
inputSchema: {
|
|
574
579
|
factId: z4.string().uuid().describe("Fact definition ID")
|
|
575
580
|
}
|
|
@@ -597,6 +602,20 @@ function registerFactTools(server, callApi) {
|
|
|
597
602
|
},
|
|
598
603
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
599
604
|
);
|
|
605
|
+
server.registerTool(
|
|
606
|
+
"lexq_facts_export",
|
|
607
|
+
{
|
|
608
|
+
title: "Export Fact Catalog",
|
|
609
|
+
description: "Export the fact catalog. The two formats carry different things: CSV is the catalog as it stands, system facts included, for reading in a spreadsheet; JSON matches the shape that batch create accepts, so it can be fed straight back in, which is why it leaves out the fields that endpoint does not take. Returns the file contents as text.",
|
|
610
|
+
inputSchema: {
|
|
611
|
+
format: z4.enum(["csv", "json"]).default("csv").describe("Export format"),
|
|
612
|
+
keyword: z4.string().optional().describe("Filter by key or name")
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
616
|
+
params: keyword ? { format, keyword } : { format }
|
|
617
|
+
})
|
|
618
|
+
);
|
|
600
619
|
}
|
|
601
620
|
|
|
602
621
|
// src/mcp/tools/deploy.ts
|
|
@@ -1110,6 +1129,18 @@ function registerReplayTools(server, callApi) {
|
|
|
1110
1129
|
},
|
|
1111
1130
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
1112
1131
|
);
|
|
1132
|
+
server.registerTool(
|
|
1133
|
+
"lexq_replay_export",
|
|
1134
|
+
{
|
|
1135
|
+
title: "Export Replay Result",
|
|
1136
|
+
description: "Export a COMPLETED window replay job. A running job is rejected \u2014 a partial result reads as the whole thing on the receiving end. The two formats carry different things: CSV holds the effect blast radius, whose columns are fixed; JSON holds the changed samples and action parameters, which nest and whose keys differ per tenant. JSON is not a superset of CSV. Returns the file contents as text.",
|
|
1137
|
+
inputSchema: {
|
|
1138
|
+
jobId: z8.string().describe("Replay job ID"),
|
|
1139
|
+
format: z8.enum(["json", "csv"]).default("json").describe("Export format")
|
|
1140
|
+
}
|
|
1141
|
+
},
|
|
1142
|
+
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
1143
|
+
);
|
|
1113
1144
|
}
|
|
1114
1145
|
|
|
1115
1146
|
// src/mcp/tools/history.ts
|
package/package.json
CHANGED
|
@@ -218,11 +218,19 @@ lexq replay start --version-id <candidateVid> --from 2025-01-01 --to 2025-01-31
|
|
|
218
218
|
lexq replay get --id <jobId>
|
|
219
219
|
lexq replay list
|
|
220
220
|
lexq replay cancel --id <jobId>
|
|
221
|
+
|
|
222
|
+
# Export a finished job
|
|
223
|
+
lexq replay export --id <jobId> --as csv --output blast-radius.csv
|
|
221
224
|
```
|
|
222
225
|
|
|
223
226
|
Replay reports whether the outcome is `DETERMINISTIC` or `REPLAY_MAY_DIFFER` — the latter when the
|
|
224
227
|
original execution depended on values the replay cannot reproduce.
|
|
225
228
|
|
|
229
|
+
Only a `COMPLETED` job can be exported; a running one has no full result yet, and a partial one reads
|
|
230
|
+
as the whole thing on the receiving end. The two formats answer different questions — CSV carries the
|
|
231
|
+
effect blast radius, whose columns are fixed, and JSON carries the changed samples and action
|
|
232
|
+
parameters, which nest and whose keys differ per tenant.
|
|
233
|
+
|
|
226
234
|
## 6. Latency Profile
|
|
227
235
|
|
|
228
236
|
Per-rule latency distribution for a policy group.
|
|
@@ -141,7 +141,7 @@ Prefixes you will encounter through the CLI:
|
|
|
141
141
|
| `A-` | Auth | Invalid or missing API key |
|
|
142
142
|
| `P-` | Policy group/version | Not found, wrong lifecycle state, already live |
|
|
143
143
|
| `ACT-` | Action parameters | Missing or invalid action parameter |
|
|
144
|
-
| `FD-` | Fact definitions | Duplicate key, system fact immutable
|
|
144
|
+
| `FD-` | Fact definitions | Duplicate key, system fact immutable, fact still referenced by rules |
|
|
145
145
|
| `AN-` | Analytics | Dry-run or simulation failure |
|
|
146
146
|
| `FL-` | Failure logs | Log not found |
|
|
147
147
|
| `WH-` | Webhook subscriptions | Invalid URL, delivery failure |
|
|
@@ -250,7 +250,7 @@ lexq analytics simulation start --json '{
|
|
|
250
250
|
```bash
|
|
251
251
|
# 1. Download template (optional)
|
|
252
252
|
lexq analytics dataset template \
|
|
253
|
-
--group-id <gid> --version-id <vid> --
|
|
253
|
+
--group-id <gid> --version-id <vid> --as csv --output template.csv
|
|
254
254
|
|
|
255
255
|
# 2. Upload dataset
|
|
256
256
|
lexq analytics dataset upload --file ./my-data.csv
|
|
@@ -304,8 +304,8 @@ lexq analytics simulation cancel --id <simulationId> --force
|
|
|
304
304
|
### Export Results
|
|
305
305
|
|
|
306
306
|
```bash
|
|
307
|
-
lexq analytics simulation export --id <simulationId> --
|
|
308
|
-
lexq analytics simulation export --id <simulationId> --
|
|
307
|
+
lexq analytics simulation export --id <simulationId> --as json
|
|
308
|
+
lexq analytics simulation export --id <simulationId> --as csv --output results.csv
|
|
309
309
|
```
|
|
310
310
|
|
|
311
311
|
## Simulation Response (COMPLETED)
|