@lexq/cli 0.1.54 → 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 +2 -2
- package/dist/index.js +133 -22
- package/dist/mcp/register.js +26 -0
- package/package.json +1 -1
- package/skills/lexq-execution/SKILL.md +8 -0
- 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
|
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);
|
|
@@ -2766,6 +2821,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2766
2821
|
process.exit(1);
|
|
2767
2822
|
}
|
|
2768
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
|
+
});
|
|
2769
2854
|
}
|
|
2770
2855
|
|
|
2771
2856
|
// src/commands/provenance.ts
|
|
@@ -3810,6 +3895,20 @@ function registerFactTools(server, callApi) {
|
|
|
3810
3895
|
},
|
|
3811
3896
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
3812
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
|
+
);
|
|
3813
3912
|
}
|
|
3814
3913
|
|
|
3815
3914
|
// src/mcp/tools/deploy.ts
|
|
@@ -4323,6 +4422,18 @@ function registerReplayTools(server, callApi) {
|
|
|
4323
4422
|
},
|
|
4324
4423
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
4325
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
|
+
);
|
|
4326
4437
|
}
|
|
4327
4438
|
|
|
4328
4439
|
// src/mcp/tools/history.ts
|
package/dist/mcp/register.js
CHANGED
|
@@ -602,6 +602,20 @@ function registerFactTools(server, callApi) {
|
|
|
602
602
|
},
|
|
603
603
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
604
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
|
+
);
|
|
605
619
|
}
|
|
606
620
|
|
|
607
621
|
// src/mcp/tools/deploy.ts
|
|
@@ -1115,6 +1129,18 @@ function registerReplayTools(server, callApi) {
|
|
|
1115
1129
|
},
|
|
1116
1130
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
1117
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
|
+
);
|
|
1118
1144
|
}
|
|
1119
1145
|
|
|
1120
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.
|
|
@@ -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)
|