@lexq/cli 0.1.54 → 0.1.56
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 +161 -44
- package/dist/mcp/register.js +33 -2
- package/package.json +2 -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,59 @@ ${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
|
+
|
|
1177
|
+
// src/types/enums.ts
|
|
1178
|
+
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
1179
|
+
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
1180
|
+
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
1181
|
+
var ProfileCacheState = ["HIT", "MISS"];
|
|
1182
|
+
var ExportFormat = ["CSV", "JSON"];
|
|
1183
|
+
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
1184
|
+
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
1185
|
+
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
1186
|
+
var PlatformEventType = [
|
|
1187
|
+
"VERSION_PUBLISHED",
|
|
1188
|
+
"DEPLOYED",
|
|
1189
|
+
"ROLLED_BACK",
|
|
1190
|
+
"UNDEPLOYED",
|
|
1191
|
+
"DEPLOY_SCHEDULED",
|
|
1192
|
+
"DEPLOY_SCHEDULE_CANCELED"
|
|
1193
|
+
];
|
|
1194
|
+
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
1195
|
+
|
|
1196
|
+
// src/lib/export.ts
|
|
1197
|
+
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
1198
|
+
function parseFormat(value) {
|
|
1199
|
+
if (typeof value === "string" && EXPORT_FORMATS.includes(value)) {
|
|
1200
|
+
return value;
|
|
1201
|
+
}
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Unsupported export format: ${String(value)}. Use ${EXPORT_FORMATS.join(" or ")}.`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
async function runExport(path, format, options) {
|
|
1207
|
+
const { output, params, ...clientOptions } = options;
|
|
1208
|
+
const body = await apiRequest("GET", path, {
|
|
1209
|
+
...clientOptions,
|
|
1210
|
+
params: { ...params, format }
|
|
1211
|
+
});
|
|
1212
|
+
if (typeof body !== "string") {
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
`Export did not return a file (${path}). Re-run with --verbose to see the response headers.`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
if (output) {
|
|
1218
|
+
writeFileSync2(output, body, "utf-8");
|
|
1219
|
+
console.log(`\u2713 Exported to ${output}`);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
process.stdout.write(body.endsWith("\n") ? body : body + "\n");
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/commands/facts.ts
|
|
1167
1226
|
function registerFactCommands(program) {
|
|
1168
1227
|
const facts = program.command("facts").description("Manage fact definitions (schema)").addHelpText(
|
|
1169
1228
|
"after",
|
|
@@ -1360,6 +1419,36 @@ ${data.length} unregistered`);
|
|
|
1360
1419
|
process.exit(1);
|
|
1361
1420
|
}
|
|
1362
1421
|
});
|
|
1422
|
+
facts.command("export").description("Export the fact catalog").option("--as <fmt>", `Exported file format: ${EXPORT_FORMATS.join(" or ")}`, "csv").option("--keyword <keyword>", "Filter by key or name").option("--output <path>", "Output file path").addHelpText(
|
|
1423
|
+
"after",
|
|
1424
|
+
dedent6`
|
|
1425
|
+
|
|
1426
|
+
The two formats carry different things. CSV is the catalog as it stands, system
|
|
1427
|
+
facts included, for reading in a spreadsheet. JSON matches the shape that
|
|
1428
|
+
batch create accepts, so it can be fed straight back in — which is why it leaves
|
|
1429
|
+
out the fields that endpoint does not take.
|
|
1430
|
+
|
|
1431
|
+
Examples:
|
|
1432
|
+
$ lexq facts export --output facts.csv
|
|
1433
|
+
$ lexq facts export --as json --output facts.json
|
|
1434
|
+
$ lexq facts export --keyword user --as json
|
|
1435
|
+
`
|
|
1436
|
+
).action(async (opts) => {
|
|
1437
|
+
try {
|
|
1438
|
+
const globalOpts = program.opts();
|
|
1439
|
+
await runExport("schema/facts/export", parseFormat(opts.as), {
|
|
1440
|
+
apiKey: globalOpts.apiKey,
|
|
1441
|
+
baseUrl: globalOpts.baseUrl,
|
|
1442
|
+
dryRun: globalOpts.dryRun,
|
|
1443
|
+
verbose: globalOpts.verbose,
|
|
1444
|
+
output: opts.output,
|
|
1445
|
+
params: opts.keyword ? { keyword: opts.keyword } : void 0
|
|
1446
|
+
});
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
printError(error);
|
|
1449
|
+
process.exit(1);
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1363
1452
|
facts.command("action-metadata").description("Get action runtime fact metadata").addHelpText(
|
|
1364
1453
|
"after",
|
|
1365
1454
|
dedent6`
|
|
@@ -1843,7 +1932,7 @@ async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
|
1843
1932
|
}
|
|
1844
1933
|
|
|
1845
1934
|
// src/commands/analytics.ts
|
|
1846
|
-
import { readFileSync as readFileSync2, writeFileSync as
|
|
1935
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1847
1936
|
import "commander";
|
|
1848
1937
|
import dedent8 from "dedent";
|
|
1849
1938
|
function registerAnalyticsCommands(program) {
|
|
@@ -2191,38 +2280,30 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2191
2280
|
process.exit(1);
|
|
2192
2281
|
}
|
|
2193
2282
|
});
|
|
2194
|
-
sim.command("export").description("Export simulation results").requiredOption("--id <simulationId>", "Simulation ID").option("--
|
|
2283
|
+
sim.command("export").description("Export simulation results").requiredOption("--id <simulationId>", "Simulation ID").option("--as <fmt>", `Exported file format: ${EXPORT_FORMATS.join(" or ")}`, "json").option("--output <path>", "Output file path").addHelpText(
|
|
2195
2284
|
"after",
|
|
2196
2285
|
dedent8`
|
|
2197
2286
|
|
|
2198
2287
|
Only COMPLETED simulations can be exported.
|
|
2199
2288
|
|
|
2200
2289
|
Examples:
|
|
2201
|
-
$ lexq analytics simulation export --id <simId> --
|
|
2202
|
-
$ lexq analytics simulation export --id <simId> --
|
|
2290
|
+
$ lexq analytics simulation export --id <simId> --as csv --output results.csv
|
|
2291
|
+
$ lexq analytics simulation export --id <simId> --as json
|
|
2203
2292
|
`
|
|
2204
2293
|
).action(async (opts) => {
|
|
2205
2294
|
try {
|
|
2206
2295
|
const globalOpts = program.opts();
|
|
2207
|
-
|
|
2208
|
-
const response = await apiRequest(
|
|
2209
|
-
"GET",
|
|
2296
|
+
await runExport(
|
|
2210
2297
|
`analytics/simulations/${opts.id}/export`,
|
|
2298
|
+
parseFormat(opts.as),
|
|
2211
2299
|
{
|
|
2212
2300
|
apiKey: globalOpts.apiKey,
|
|
2213
2301
|
baseUrl: globalOpts.baseUrl,
|
|
2214
2302
|
dryRun: globalOpts.dryRun,
|
|
2215
2303
|
verbose: globalOpts.verbose,
|
|
2216
|
-
|
|
2304
|
+
output: opts.output
|
|
2217
2305
|
}
|
|
2218
2306
|
);
|
|
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
2307
|
} catch (error) {
|
|
2227
2308
|
printError(error);
|
|
2228
2309
|
process.exit(1);
|
|
@@ -2293,7 +2374,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2293
2374
|
process.exit(1);
|
|
2294
2375
|
}
|
|
2295
2376
|
});
|
|
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("--
|
|
2377
|
+
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: ${EXPORT_FORMATS.join(" or ")}`, "csv").option("--output <path>", "Output file path").addHelpText(
|
|
2297
2378
|
"after",
|
|
2298
2379
|
dedent8`
|
|
2299
2380
|
|
|
@@ -2301,7 +2382,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2301
2382
|
|
|
2302
2383
|
Examples:
|
|
2303
2384
|
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
|
|
2304
|
-
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --
|
|
2385
|
+
$ lexq analytics dataset template --group-id <gid> --version-id <vid> --as json
|
|
2305
2386
|
`
|
|
2306
2387
|
).action(async (opts) => {
|
|
2307
2388
|
try {
|
|
@@ -2312,7 +2393,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2312
2393
|
if (!apiKey) {
|
|
2313
2394
|
throw new Error('Not authenticated. Run "lexq auth login" first.');
|
|
2314
2395
|
}
|
|
2315
|
-
const fmt = opts.
|
|
2396
|
+
const fmt = parseFormat(opts.as);
|
|
2316
2397
|
const url = new URL(
|
|
2317
2398
|
`analytics/groups/${opts.groupId}/versions/${opts.versionId}/dataset-template`,
|
|
2318
2399
|
baseUrl.endsWith("/") ? baseUrl : baseUrl + "/"
|
|
@@ -2326,7 +2407,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2326
2407
|
}
|
|
2327
2408
|
const text = await response.text();
|
|
2328
2409
|
if (opts.output) {
|
|
2329
|
-
|
|
2410
|
+
writeFileSync3(opts.output, text, "utf-8");
|
|
2330
2411
|
console.log(`\u2713 Template saved to ${opts.output}`);
|
|
2331
2412
|
} else {
|
|
2332
2413
|
console.log(text);
|
|
@@ -2357,26 +2438,6 @@ function resolveBody(opts) {
|
|
|
2357
2438
|
// src/commands/profile.ts
|
|
2358
2439
|
import "commander";
|
|
2359
2440
|
import dedent9 from "dedent";
|
|
2360
|
-
|
|
2361
|
-
// src/types/enums.ts
|
|
2362
|
-
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
2363
|
-
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
2364
|
-
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
2365
|
-
var ProfileCacheState = ["HIT", "MISS"];
|
|
2366
|
-
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
2367
|
-
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
2368
|
-
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
2369
|
-
var PlatformEventType = [
|
|
2370
|
-
"VERSION_PUBLISHED",
|
|
2371
|
-
"DEPLOYED",
|
|
2372
|
-
"ROLLED_BACK",
|
|
2373
|
-
"UNDEPLOYED",
|
|
2374
|
-
"DEPLOY_SCHEDULED",
|
|
2375
|
-
"DEPLOY_SCHEDULE_CANCELED"
|
|
2376
|
-
];
|
|
2377
|
-
var WebhookPayloadFormat = ["GENERIC", "SLACK"];
|
|
2378
|
-
|
|
2379
|
-
// src/commands/profile.ts
|
|
2380
2441
|
var ms = (nanos) => nanos == null ? "\u2013" : (nanos / 1e6).toFixed(2);
|
|
2381
2442
|
var msWithUnit = (nanos) => nanos == null ? "\u2013" : `${(nanos / 1e6).toFixed(2)}ms`;
|
|
2382
2443
|
function registerProfileCommands(program) {
|
|
@@ -2766,6 +2827,36 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2766
2827
|
process.exit(1);
|
|
2767
2828
|
}
|
|
2768
2829
|
});
|
|
2830
|
+
replay.command("export").description("Export the result of a completed replay job").requiredOption("--id <jobId>", "Replay job ID").option("--as <fmt>", `Exported file format: ${EXPORT_FORMATS.join(" or ")}`, "json").option("--output <path>", "Output file path").addHelpText(
|
|
2831
|
+
"after",
|
|
2832
|
+
dedent11`
|
|
2833
|
+
|
|
2834
|
+
Only COMPLETED jobs can be exported. A running job has no full result yet, and a
|
|
2835
|
+
partial one reads as the whole thing on the receiving end.
|
|
2836
|
+
|
|
2837
|
+
The two formats carry different things. CSV holds the effect blast radius, whose
|
|
2838
|
+
columns are fixed. JSON holds the changed samples and action parameters, which
|
|
2839
|
+
nest and whose keys differ per tenant. JSON is not a superset of CSV.
|
|
2840
|
+
|
|
2841
|
+
Examples:
|
|
2842
|
+
$ lexq replay export --id <jobId> --as csv --output blast-radius.csv
|
|
2843
|
+
$ lexq replay export --id <jobId> --as json
|
|
2844
|
+
`
|
|
2845
|
+
).action(async (opts) => {
|
|
2846
|
+
try {
|
|
2847
|
+
const globalOpts = program.opts();
|
|
2848
|
+
await runExport(`replay/jobs/${opts.id}/export`, parseFormat(opts.as), {
|
|
2849
|
+
apiKey: globalOpts.apiKey,
|
|
2850
|
+
baseUrl: globalOpts.baseUrl,
|
|
2851
|
+
dryRun: globalOpts.dryRun,
|
|
2852
|
+
verbose: globalOpts.verbose,
|
|
2853
|
+
output: opts.output
|
|
2854
|
+
});
|
|
2855
|
+
} catch (error) {
|
|
2856
|
+
printError(error);
|
|
2857
|
+
process.exit(1);
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2769
2860
|
}
|
|
2770
2861
|
|
|
2771
2862
|
// src/commands/provenance.ts
|
|
@@ -3810,6 +3901,20 @@ function registerFactTools(server, callApi) {
|
|
|
3810
3901
|
},
|
|
3811
3902
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
3812
3903
|
);
|
|
3904
|
+
server.registerTool(
|
|
3905
|
+
"lexq_facts_export",
|
|
3906
|
+
{
|
|
3907
|
+
title: "Export Fact Catalog",
|
|
3908
|
+
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.",
|
|
3909
|
+
inputSchema: {
|
|
3910
|
+
format: z4.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
3911
|
+
keyword: z4.string().optional().describe("Filter by key or name")
|
|
3912
|
+
}
|
|
3913
|
+
},
|
|
3914
|
+
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
3915
|
+
params: keyword ? { format, keyword } : { format }
|
|
3916
|
+
})
|
|
3917
|
+
);
|
|
3813
3918
|
}
|
|
3814
3919
|
|
|
3815
3920
|
// src/mcp/tools/deploy.ts
|
|
@@ -4139,7 +4244,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4139
4244
|
description: "Export simulation results as JSON or CSV. Returns the raw data.",
|
|
4140
4245
|
inputSchema: {
|
|
4141
4246
|
simulationId: z6.string().uuid().describe("Simulation ID"),
|
|
4142
|
-
format: z6.enum(
|
|
4247
|
+
format: z6.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4143
4248
|
}
|
|
4144
4249
|
},
|
|
4145
4250
|
async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
@@ -4197,7 +4302,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
4197
4302
|
inputSchema: {
|
|
4198
4303
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
4199
4304
|
versionId: z6.string().uuid().describe("Version ID"),
|
|
4200
|
-
format: z6.enum(
|
|
4305
|
+
format: z6.enum(EXPORT_FORMATS).default("csv").describe("Template format")
|
|
4201
4306
|
}
|
|
4202
4307
|
},
|
|
4203
4308
|
async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
|
|
@@ -4323,6 +4428,18 @@ function registerReplayTools(server, callApi) {
|
|
|
4323
4428
|
},
|
|
4324
4429
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
4325
4430
|
);
|
|
4431
|
+
server.registerTool(
|
|
4432
|
+
"lexq_replay_export",
|
|
4433
|
+
{
|
|
4434
|
+
title: "Export Replay Result",
|
|
4435
|
+
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.",
|
|
4436
|
+
inputSchema: {
|
|
4437
|
+
jobId: z8.string().describe("Replay job ID"),
|
|
4438
|
+
format: z8.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
4439
|
+
}
|
|
4440
|
+
},
|
|
4441
|
+
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
4442
|
+
);
|
|
4326
4443
|
}
|
|
4327
4444
|
|
|
4328
4445
|
// src/mcp/tools/history.ts
|
package/dist/mcp/register.js
CHANGED
|
@@ -18,6 +18,7 @@ import { z } from "zod";
|
|
|
18
18
|
var ValueType = ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
19
19
|
var ConflictResolutionMode = ["NONE", "EXCLUSIVE", "MAX_N"];
|
|
20
20
|
var ConflictResolutionStrategy = ["HIGHEST_PRIORITY"];
|
|
21
|
+
var ExportFormat = ["CSV", "JSON"];
|
|
21
22
|
var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
|
|
22
23
|
var FailureAction = ["IGNORE", "RESOLVE"];
|
|
23
24
|
var TaskType = ["PLATFORM_WEBHOOK", "SCHEDULED_DEPLOYMENT"];
|
|
@@ -514,6 +515,10 @@ import { z as z4 } from "zod";
|
|
|
514
515
|
// src/types/facts.ts
|
|
515
516
|
var FACT_KEY_PATTERN = /^[a-zA-Z][a-zA-Z0-9_]*$/;
|
|
516
517
|
|
|
518
|
+
// src/lib/export.ts
|
|
519
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
520
|
+
var EXPORT_FORMATS = ExportFormat.map((name) => name.toLowerCase()).sort();
|
|
521
|
+
|
|
517
522
|
// src/mcp/tools/facts.ts
|
|
518
523
|
var VALUE_TYPES = ValueType;
|
|
519
524
|
function registerFactTools(server, callApi) {
|
|
@@ -602,6 +607,20 @@ function registerFactTools(server, callApi) {
|
|
|
602
607
|
},
|
|
603
608
|
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
604
609
|
);
|
|
610
|
+
server.registerTool(
|
|
611
|
+
"lexq_facts_export",
|
|
612
|
+
{
|
|
613
|
+
title: "Export Fact Catalog",
|
|
614
|
+
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.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
format: z4.enum(EXPORT_FORMATS).default("csv").describe("Export format"),
|
|
617
|
+
keyword: z4.string().optional().describe("Filter by key or name")
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
async ({ format, keyword }) => callApi("GET", "schema/facts/export", {
|
|
621
|
+
params: keyword ? { format, keyword } : { format }
|
|
622
|
+
})
|
|
623
|
+
);
|
|
605
624
|
}
|
|
606
625
|
|
|
607
626
|
// src/mcp/tools/deploy.ts
|
|
@@ -931,7 +950,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
931
950
|
description: "Export simulation results as JSON or CSV. Returns the raw data.",
|
|
932
951
|
inputSchema: {
|
|
933
952
|
simulationId: z6.string().uuid().describe("Simulation ID"),
|
|
934
|
-
format: z6.enum(
|
|
953
|
+
format: z6.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
935
954
|
}
|
|
936
955
|
},
|
|
937
956
|
async ({ simulationId, format }) => callApi("GET", `analytics/simulations/${simulationId}/export`, {
|
|
@@ -989,7 +1008,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
989
1008
|
inputSchema: {
|
|
990
1009
|
groupId: z6.string().uuid().describe("Policy group ID"),
|
|
991
1010
|
versionId: z6.string().uuid().describe("Version ID"),
|
|
992
|
-
format: z6.enum(
|
|
1011
|
+
format: z6.enum(EXPORT_FORMATS).default("csv").describe("Template format")
|
|
993
1012
|
}
|
|
994
1013
|
},
|
|
995
1014
|
async ({ groupId, versionId, format }) => callApi("GET", `analytics/groups/${groupId}/versions/${versionId}/dataset-template`, {
|
|
@@ -1115,6 +1134,18 @@ function registerReplayTools(server, callApi) {
|
|
|
1115
1134
|
},
|
|
1116
1135
|
async ({ jobId }) => callApi("POST", `replay/jobs/${jobId}/cancel`)
|
|
1117
1136
|
);
|
|
1137
|
+
server.registerTool(
|
|
1138
|
+
"lexq_replay_export",
|
|
1139
|
+
{
|
|
1140
|
+
title: "Export Replay Result",
|
|
1141
|
+
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.",
|
|
1142
|
+
inputSchema: {
|
|
1143
|
+
jobId: z8.string().describe("Replay job ID"),
|
|
1144
|
+
format: z8.enum(EXPORT_FORMATS).default("json").describe("Export format")
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
async ({ jobId, format }) => callApi("GET", `replay/jobs/${jobId}/export`, { params: { format } })
|
|
1148
|
+
);
|
|
1118
1149
|
}
|
|
1119
1150
|
|
|
1120
1151
|
// src/mcp/tools/history.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lexq/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
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": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"start": "node dist/index.js",
|
|
36
36
|
"test:decimals": "pnpm build && node tests/exact-decimals.mjs",
|
|
37
37
|
"test:fact-key": "node tests/fact-key.mjs",
|
|
38
|
+
"test:export-format": "node tests/export-format.mjs",
|
|
38
39
|
"prepublishOnly": "pnpm build",
|
|
39
40
|
"knip": "knip",
|
|
40
41
|
"format": "prettier --write \"src/**/*.ts\"",
|
|
@@ -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)
|