@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 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
- const contentType = response.headers.get("content-type") ?? "";
130
- if (contentType.includes("text/csv") || contentType.includes("application/octet-stream")) {
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 writeFileSync2 } from "fs";
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("--format <fmt>", "Export format: csv or json", "json").option("--output <path>", "Output file path").addHelpText(
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> --format csv --output results.csv
2202
- $ lexq analytics simulation export --id <simId> --format json
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
- const exportFormat = opts.format === "csv" ? "csv" : "json";
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
- params: { format: exportFormat }
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("--format <fmt>", "Template format: csv or json", "csv").option("--output <path>", "Output file path").addHelpText(
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> --format json
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.format === "json" ? "json" : "csv";
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
- writeFileSync2(opts.output, text, "utf-8");
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(["json", "csv"]).default("json").describe("Export format")
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(["csv", "json"]).default("csv").describe("Template format")
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
@@ -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(["json", "csv"]).default("json").describe("Export format")
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(["csv", "json"]).default("csv").describe("Template format")
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.54",
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> --format csv --output template.csv
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> --format json
308
- lexq analytics simulation export --id <simulationId> --format csv --output results.csv
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)