@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 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,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 writeFileSync2 } from "fs";
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("--format <fmt>", "Export format: csv or json", "json").option("--output <path>", "Output file path").addHelpText(
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> --format csv --output results.csv
2202
- $ lexq analytics simulation export --id <simId> --format json
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
- const exportFormat = opts.format === "csv" ? "csv" : "json";
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
- params: { format: exportFormat }
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("--format <fmt>", "Template format: csv or json", "csv").option("--output <path>", "Output file path").addHelpText(
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> --format json
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.format === "json" ? "json" : "csv";
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
- writeFileSync2(opts.output, text, "utf-8");
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lexq/cli",
3
- "version": "0.1.54",
3
+ "version": "0.1.55",
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": {
@@ -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)