@lexq/cli 0.1.18 → 0.1.20

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/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { dirname as dirname2, join as join3 } from "path";
10
10
  import "commander";
11
11
  import { createInterface } from "readline/promises";
12
12
  import { stdin, stdout } from "process";
13
+ import dedent from "dedent";
13
14
 
14
15
  // src/lib/config.ts
15
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
@@ -165,8 +166,32 @@ function printError(error) {
165
166
 
166
167
  // src/commands/auth.ts
167
168
  function registerAuthCommands(program) {
168
- const auth = program.command("auth").description("Manage authentication");
169
- auth.command("login").description("Authenticate with your LexQ API key").action(async () => {
169
+ const auth = program.command("auth").description("Manage authentication").addHelpText(
170
+ "after",
171
+ dedent`
172
+
173
+ Commands:
174
+ login Save your API key locally
175
+ logout Remove stored credentials
176
+ whoami Verify authentication and show account info
177
+
178
+ Getting Started:
179
+ 1. Get your API key from the LexQ Console (Settings → API Keys)
180
+ 2. Run: lexq auth login
181
+ 3. Verify: lexq auth whoami
182
+ `
183
+ );
184
+ auth.command("login").description("Authenticate with your LexQ API key").addHelpText(
185
+ "after",
186
+ dedent`
187
+
188
+ Example:
189
+ $ lexq auth login
190
+ Enter your API Key: sk_live_****
191
+
192
+ ✓ API key saved to ~/.lexq/config.json
193
+ `
194
+ ).action(async () => {
170
195
  try {
171
196
  const rl = createInterface({ input: stdin, output: stdout });
172
197
  const apiKey = await rl.question("Enter your API Key: ");
@@ -187,22 +212,35 @@ function registerAuthCommands(program) {
187
212
  deleteConfig();
188
213
  console.log("\u2713 Credentials removed.");
189
214
  });
190
- auth.command("whoami").description("Show current authentication info").action(async () => {
215
+ auth.command("whoami").description("Show current authentication info").addHelpText(
216
+ "after",
217
+ dedent`
218
+
219
+ Example:
220
+ $ lexq auth whoami
221
+ { "tenantId": "abc-123", "userId": "...", "role": "ADMIN", "apiKey": "sk_live_****abcd" }
222
+ `
223
+ ).action(async () => {
191
224
  try {
225
+ const globalOpts = program.opts();
192
226
  const config = loadConfig();
193
- if (!config.apiKey) {
227
+ const apiKey = globalOpts.apiKey ?? config.apiKey;
228
+ const baseUrl = globalOpts.baseUrl ?? config.baseUrl;
229
+ if (!apiKey) {
194
230
  console.error('Not authenticated. Run "lexq auth login" first.');
195
231
  process.exit(1);
196
232
  }
197
233
  const info = await apiRequest("GET", "whoami", {
198
- apiKey: config.apiKey,
199
- baseUrl: config.baseUrl
234
+ apiKey,
235
+ baseUrl,
236
+ dryRun: globalOpts.dryRun,
237
+ verbose: globalOpts.verbose
200
238
  });
201
- const masked = config.apiKey.length > 8 ? config.apiKey.substring(0, 4) + "****" + config.apiKey.substring(config.apiKey.length - 4) : "****";
239
+ const masked = apiKey.length > 8 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4) : "****";
202
240
  printJson({
203
241
  ...info,
204
242
  apiKey: masked,
205
- baseUrl: config.baseUrl
243
+ baseUrl
206
244
  });
207
245
  } catch (error) {
208
246
  printError(error);
@@ -213,8 +251,19 @@ function registerAuthCommands(program) {
213
251
 
214
252
  // src/commands/status.ts
215
253
  import "commander";
254
+ import dedent2 from "dedent";
216
255
  function registerStatusCommand(program) {
217
- program.command("status").description("Check API connectivity and authentication").action(async () => {
256
+ program.command("status").description("Check API connectivity and authentication").addHelpText(
257
+ "after",
258
+ dedent2`
259
+
260
+ Example:
261
+ $ lexq status
262
+ { "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "ADMIN" }
263
+
264
+ Use this to verify your API key is valid and the LexQ API is reachable.
265
+ `
266
+ ).action(async () => {
218
267
  const globalOpts = program.opts();
219
268
  const startTime = Date.now();
220
269
  try {
@@ -237,8 +286,26 @@ function registerStatusCommand(program) {
237
286
 
238
287
  // src/commands/groups.ts
239
288
  import "commander";
289
+ import dedent3 from "dedent";
240
290
  function registerGroupCommands(program) {
241
- const groups = program.command("groups").description("Manage policy groups");
291
+ const groups = program.command("groups").description("Manage policy groups").addHelpText(
292
+ "after",
293
+ dedent3`
294
+
295
+ A policy group is the top-level container for rule versions.
296
+ It controls deployment lifecycle, conflict resolution, and A/B testing.
297
+
298
+ Commands:
299
+ list List all policy groups
300
+ get Get group detail by ID
301
+ create Create a new group
302
+ update Update group settings
303
+ delete Archive a group
304
+ ab-test Manage A/B tests (start, stop, adjust)
305
+
306
+ Statuses: ACTIVE, DISABLED (emergency stop), ARCHIVED (soft delete)
307
+ `
308
+ );
242
309
  groups.command("list").description("List all policy groups").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
243
310
  try {
244
311
  const globalOpts = program.opts();
@@ -288,7 +355,28 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
288
355
  process.exit(1);
289
356
  }
290
357
  });
291
- groups.command("create").description("Create a new policy group").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
358
+ groups.command("create").description("Create a new policy group").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
359
+ "after",
360
+ dedent3`
361
+
362
+ Example:
363
+ $ lexq groups create --json '{
364
+ "name": "Payment Policy",
365
+ "description": "Rules for payment processing",
366
+ "priority": 0
367
+ }'
368
+
369
+ Fields:
370
+ name string Group name (required, unique per tenant)
371
+ description string Description (optional, max 255 chars)
372
+ priority number Execution priority — lower runs first (required, min 0)
373
+ activationGroup string Cross-group conflict resolution key (optional)
374
+ activationMode string NONE | EXCLUSIVE | MAX_N [default: NONE]
375
+ activationStrategy string FIRST_MATCH | HIGHEST_PRIORITY | MAX_BENEFIT [default: FIRST_MATCH]
376
+ executionLimit number Max rules to fire in MAX_N mode (optional)
377
+ status string ACTIVE | DISABLED [default: ACTIVE]
378
+ `
379
+ ).action(async (opts) => {
292
380
  try {
293
381
  const globalOpts = program.opts();
294
382
  const body = JSON.parse(opts.json);
@@ -305,7 +393,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
305
393
  process.exit(1);
306
394
  }
307
395
  });
308
- groups.command("update").description("Update a policy group").requiredOption("--id <groupId>", "Policy group ID").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
396
+ groups.command("update").description("Update a policy group").requiredOption("--id <groupId>", "Policy group ID").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
397
+ "after",
398
+ dedent3`
399
+
400
+ Example:
401
+ $ lexq groups update --id <groupId> --json '{"description": "Updated", "priority": 1}'
402
+
403
+ All fields are optional — only provided fields are updated.
404
+ `
405
+ ).action(async (opts) => {
309
406
  try {
310
407
  const globalOpts = program.opts();
311
408
  const body = JSON.parse(opts.json);
@@ -322,7 +419,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
322
419
  process.exit(1);
323
420
  }
324
421
  });
325
- groups.command("delete").description("Delete a policy group").requiredOption("--id <groupId>", "Policy group ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
422
+ groups.command("delete").description("Delete a policy group").requiredOption("--id <groupId>", "Policy group ID").option("--force", "Skip confirmation prompt").addHelpText(
423
+ "after",
424
+ dedent3`
425
+
426
+ This archives the group (soft delete). Use --force to skip the confirmation prompt.
427
+ `
428
+ ).action(async (opts) => {
326
429
  try {
327
430
  const globalOpts = program.opts();
328
431
  if (!opts.force) {
@@ -347,8 +450,31 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
347
450
  process.exit(1);
348
451
  }
349
452
  });
350
- const abTest = groups.command("ab-test").description("A/B test management");
351
- abTest.command("start").description("Start an A/B test").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Challenger version ID").requiredOption("--traffic-rate <rate>", "Traffic rate for challenger (1-99)").action(async (opts) => {
453
+ const abTest = groups.command("ab-test").description("A/B test management").addHelpText(
454
+ "after",
455
+ dedent3`
456
+
457
+ Split traffic between the current live version and a challenger version.
458
+
459
+ Commands:
460
+ start Start an A/B test with a challenger version
461
+ stop Stop the test and revert to 100% live version
462
+ adjust Change the traffic percentage
463
+
464
+ The traffic rate (1-99) determines what percentage goes to the challenger.
465
+ The remaining traffic continues to the current live version.
466
+ `
467
+ );
468
+ abTest.command("start").description("Start an A/B test").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Challenger version ID").requiredOption("--traffic-rate <rate>", "Traffic rate for challenger (1-99)").addHelpText(
469
+ "after",
470
+ dedent3`
471
+
472
+ Example:
473
+ $ lexq groups ab-test start --group-id <gid> --version-id <vid> --traffic-rate 20
474
+
475
+ Routes 20% of traffic to the challenger version, 80% to the current live version.
476
+ `
477
+ ).action(async (opts) => {
352
478
  try {
353
479
  const globalOpts = program.opts();
354
480
  const body = {
@@ -397,7 +523,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
397
523
  process.exit(1);
398
524
  }
399
525
  });
400
- abTest.command("adjust").description("Adjust A/B test traffic rate").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--traffic-rate <rate>", "New traffic rate (1-99)").action(async (opts) => {
526
+ abTest.command("adjust").description("Adjust A/B test traffic rate").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--traffic-rate <rate>", "New traffic rate (1-99)").addHelpText(
527
+ "after",
528
+ dedent3`
529
+
530
+ Example:
531
+ $ lexq groups ab-test adjust --group-id <gid> --traffic-rate 50
532
+ `
533
+ ).action(async (opts) => {
401
534
  try {
402
535
  const globalOpts = program.opts();
403
536
  const body = {
@@ -424,8 +557,27 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
424
557
 
425
558
  // src/commands/versions.ts
426
559
  import "commander";
560
+ import dedent4 from "dedent";
427
561
  function registerVersionCommands(program) {
428
- const versions = program.command("versions").description("Manage policy versions");
562
+ const versions = program.command("versions").description("Manage policy versions").addHelpText(
563
+ "after",
564
+ dedent4`
565
+
566
+ A version is an immutable snapshot of rules within a policy group.
567
+
568
+ Lifecycle: DRAFT → ACTIVE (publish) → ARCHIVED (superseded) | EXPIRED (past effectiveTo)
569
+
570
+ Commands:
571
+ list List versions for a group
572
+ get Get version detail
573
+ create Create a new DRAFT version
574
+ update Update DRAFT version metadata
575
+ delete Delete a DRAFT version
576
+ clone Duplicate a version (creates a new DRAFT with same rules)
577
+
578
+ Only DRAFT versions can be modified. Published versions are locked.
579
+ `
580
+ );
429
581
  versions.command("list").description("List versions for a policy group").requiredOption("--group-id <groupId>", "Policy group ID").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
430
582
  try {
431
583
  const globalOpts = program.opts();
@@ -483,7 +635,25 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
483
635
  process.exit(1);
484
636
  }
485
637
  });
486
- versions.command("create").description("Create a new draft version").requiredOption("--group-id <groupId>", "Policy group ID").option("--commit-message <message>", "Commit message").option("--effective-from <date>", "Effective from (ISO datetime)").option("--effective-to <date>", "Effective to (ISO datetime)").option("--json <body>", "Full request body as JSON (overrides other options)").action(async (opts) => {
638
+ versions.command("create").description("Create a new draft version").requiredOption("--group-id <groupId>", "Policy group ID").option("--commit-message <message>", "Commit message").option("--effective-from <date>", "Effective from (ISO datetime)").option("--effective-to <date>", "Effective to (ISO datetime)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
639
+ "after",
640
+ dedent4`
641
+
642
+ Examples:
643
+ $ lexq versions create --group-id <gid> --commit-message "Initial version"
644
+
645
+ $ lexq versions create --group-id <gid> --json '{
646
+ "commitMessage": "Seasonal promo",
647
+ "effectiveFrom": "2026-06-01T00:00:00Z",
648
+ "effectiveTo": "2026-08-31T23:59:59Z"
649
+ }'
650
+
651
+ Fields:
652
+ commitMessage string Version description (optional, max 255 chars)
653
+ effectiveFrom datetime Start of effective period (optional, ISO-8601)
654
+ effectiveTo datetime End of effective period (optional, auto-expires)
655
+ `
656
+ ).action(async (opts) => {
487
657
  try {
488
658
  const globalOpts = program.opts();
489
659
  const body = opts.json ? JSON.parse(opts.json) : buildCreateBody(opts);
@@ -504,7 +674,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
504
674
  process.exit(1);
505
675
  }
506
676
  });
507
- versions.command("update").description("Update a draft version metadata").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--commit-message <message>", "Commit message").option("--effective-from <date>", "Effective from (ISO datetime)").option("--effective-to <date>", "Effective to (ISO datetime)").option("--json <body>", "Full request body as JSON (overrides other options)").action(async (opts) => {
677
+ versions.command("update").description("Update a draft version metadata").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--commit-message <message>", "Commit message").option("--effective-from <date>", "Effective from (ISO datetime)").option("--effective-to <date>", "Effective to (ISO datetime)").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
678
+ "after",
679
+ dedent4`
680
+
681
+ Only DRAFT versions can be updated. Published (ACTIVE) versions are immutable.
682
+
683
+ Example:
684
+ $ lexq versions update --group-id <gid> --id <vid> --commit-message "Updated rules"
685
+ `
686
+ ).action(async (opts) => {
508
687
  try {
509
688
  const globalOpts = program.opts();
510
689
  const body = opts.json ? JSON.parse(opts.json) : buildUpdateBody(opts);
@@ -525,7 +704,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
525
704
  process.exit(1);
526
705
  }
527
706
  });
528
- versions.command("delete").description("Delete a policy version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
707
+ versions.command("delete").description("Delete a policy version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Policy version ID").option("--force", "Skip confirmation prompt").addHelpText(
708
+ "after",
709
+ dedent4`
710
+
711
+ Only DRAFT versions can be deleted. Use --force to skip confirmation.
712
+ `
713
+ ).action(async (opts) => {
529
714
  try {
530
715
  const globalOpts = program.opts();
531
716
  if (!opts.force) {
@@ -550,7 +735,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
550
735
  process.exit(1);
551
736
  }
552
737
  });
553
- versions.command("clone").description("Clone (duplicate) a policy version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Source version ID to clone").action(async (opts) => {
738
+ versions.command("clone").description("Clone (duplicate) a policy version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--id <versionId>", "Source version ID to clone").addHelpText(
739
+ "after",
740
+ dedent4`
741
+
742
+ Creates a new DRAFT version with all rules copied from the source.
743
+ Use this to iterate on a published version without modifying it.
744
+
745
+ Example:
746
+ $ lexq versions clone --group-id <gid> --id <source-vid>
747
+ `
748
+ ).action(async (opts) => {
554
749
  try {
555
750
  const globalOpts = program.opts();
556
751
  const data = await apiRequest(
@@ -587,8 +782,27 @@ function buildUpdateBody(opts) {
587
782
 
588
783
  // src/commands/rules.ts
589
784
  import "commander";
785
+ import dedent5 from "dedent";
590
786
  function registerRuleCommands(program) {
591
- const rules = program.command("rules").description("Manage policy rules");
787
+ const rules = program.command("rules").description("Manage policy rules").addHelpText(
788
+ "after",
789
+ dedent5`
790
+
791
+ Rules define condition → action pairs within a version.
792
+ They are evaluated in priority order (lower number = higher priority).
793
+
794
+ Commands:
795
+ list List rules in a version
796
+ get Get rule detail
797
+ create Add a new rule to a DRAFT version
798
+ update Modify a rule in a DRAFT version
799
+ delete Remove a rule from a DRAFT version
800
+ reorder Change rule priorities (drag & drop equivalent)
801
+ toggle Enable or disable a rule
802
+
803
+ Only DRAFT versions allow rule modifications.
804
+ `
805
+ );
592
806
  rules.command("list").description("List rules for a policy version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
593
807
  try {
594
808
  const globalOpts = program.opts();
@@ -646,7 +860,41 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
646
860
  process.exit(1);
647
861
  }
648
862
  });
649
- rules.command("create").description("Create a new rule").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
863
+ rules.command("create").description("Create a new rule").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
864
+ "after",
865
+ dedent5`
866
+
867
+ Example:
868
+ $ lexq rules create --group-id <gid> --version-id <vid> --json '{
869
+ "name": "VIP 20% Discount",
870
+ "priority": 0,
871
+ "condition": {
872
+ "type": "SINGLE",
873
+ "field": "customer_tier",
874
+ "operator": "EQUALS",
875
+ "value": "VIP",
876
+ "valueType": "STRING"
877
+ },
878
+ "actions": [
879
+ { "type": "DISCOUNT", "parameters": { "method": "PERCENTAGE", "rate": 20, "refVar": "payment_amount" } }
880
+ ]
881
+ }'
882
+
883
+ Condition Operators:
884
+ EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL,
885
+ LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
886
+
887
+ Action Types:
888
+ DISCOUNT, POINT, COUPON_ISSUE, BLOCK, NOTIFICATION, WEBHOOK, SET_FACT, ADD_TAG
889
+
890
+ Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
891
+
892
+ Mutex (optional — rule-level conflict resolution):
893
+ mutexGroup string Logical grouping key (e.g., "best-discount")
894
+ mutexMode string NONE | EXCLUSIVE [default: NONE]
895
+ mutexStrategy string FIRST_MATCH | HIGHEST_PRIORITY | MAX_BENEFIT
896
+ `
897
+ ).action(async (opts) => {
650
898
  try {
651
899
  const globalOpts = program.opts();
652
900
  const body = JSON.parse(opts.json);
@@ -667,7 +915,21 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
667
915
  process.exit(1);
668
916
  }
669
917
  });
670
- rules.command("update").description("Update a rule").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--id <ruleId>", "Rule ID").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
918
+ rules.command("update").description("Update a rule").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--id <ruleId>", "Rule ID").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
919
+ "after",
920
+ dedent5`
921
+
922
+ Same fields as create. Only DRAFT versions can be modified.
923
+
924
+ Example:
925
+ $ lexq rules update --group-id <gid> --version-id <vid> --id <rid> --json '{
926
+ "name": "VIP 25% Discount",
927
+ "actions": [
928
+ { "type": "DISCOUNT", "parameters": { "method": "PERCENTAGE", "rate": 25, "refVar": "payment_amount" } }
929
+ ]
930
+ }'
931
+ `
932
+ ).action(async (opts) => {
671
933
  try {
672
934
  const globalOpts = program.opts();
673
935
  const body = JSON.parse(opts.json);
@@ -717,7 +979,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
717
979
  process.exit(1);
718
980
  }
719
981
  });
720
- rules.command("reorder").description("Reorder rules by priority (drag & drop equivalent)").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--rule-ids <ids>", "Comma-separated rule IDs in desired order").action(async (opts) => {
982
+ rules.command("reorder").description("Reorder rules by priority (drag & drop equivalent)").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--rule-ids <ids>", "Comma-separated rule IDs in desired order").addHelpText(
983
+ "after",
984
+ dedent5`
985
+
986
+ Assigns priority 0, 1, 2, ... to rules in the order given.
987
+
988
+ Example:
989
+ $ lexq rules reorder --group-id <gid> --version-id <vid> --rule-ids "id3,id1,id2"
990
+
991
+ Result: id3 → priority 0, id1 → priority 1, id2 → priority 2
992
+ `
993
+ ).action(async (opts) => {
721
994
  try {
722
995
  const globalOpts = program.opts();
723
996
  const ruleIds = opts.ruleIds.split(",").map((id) => id.trim());
@@ -744,7 +1017,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
744
1017
  process.exit(1);
745
1018
  }
746
1019
  });
747
- rules.command("toggle").description("Enable or disable a rule").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--id <ruleId>", "Rule ID").requiredOption("--enabled <boolean>", "true or false").action(async (opts) => {
1020
+ rules.command("toggle").description("Enable or disable a rule").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").requiredOption("--id <ruleId>", "Rule ID").requiredOption("--enabled <boolean>", "true or false").addHelpText(
1021
+ "after",
1022
+ dedent5`
1023
+
1024
+ Disabled rules are skipped during execution without deleting them.
1025
+
1026
+ Example:
1027
+ $ lexq rules toggle --group-id <gid> --version-id <vid> --id <rid> --enabled false
1028
+ `
1029
+ ).action(async (opts) => {
748
1030
  try {
749
1031
  const globalOpts = program.opts();
750
1032
  const isEnabled = opts.enabled === "true";
@@ -769,16 +1051,30 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
769
1051
 
770
1052
  // src/commands/facts.ts
771
1053
  import "commander";
1054
+ import dedent6 from "dedent";
772
1055
  function registerFactCommands(program) {
773
- const facts = program.command("facts").description("Manage fact definitions (schema)");
1056
+ const facts = program.command("facts").description("Manage fact definitions (schema)").addHelpText(
1057
+ "after",
1058
+ dedent6`
1059
+
1060
+ Facts are input variables passed during policy execution.
1061
+ Define them here so rules can reference them in conditions and actions.
1062
+
1063
+ Commands:
1064
+ list List all fact definitions
1065
+ create Register a new fact
1066
+ update Update fact metadata
1067
+ delete Remove a fact definition
1068
+ action-metadata Show action runtime fact metadata
1069
+
1070
+ System facts (payment_amount, user_id, etc.) are auto-created and immutable.
1071
+ `
1072
+ );
774
1073
  facts.command("list").description("List fact definitions").option("--keyword <keyword>", "Filter by keyword").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
775
1074
  try {
776
1075
  const globalOpts = program.opts();
777
1076
  const format = globalOpts.format ?? "json";
778
- const params = {
779
- page: opts.page,
780
- size: opts.size
781
- };
1077
+ const params = { page: opts.page, size: opts.size };
782
1078
  if (opts.keyword) params.keyword = opts.keyword;
783
1079
  const data = await apiRequest("GET", "schema/facts", {
784
1080
  apiKey: globalOpts.apiKey,
@@ -810,7 +1106,26 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
810
1106
  process.exit(1);
811
1107
  }
812
1108
  });
813
- facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (lowercase, underscores)").option("--name <name>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--json <body>", "Full request body as JSON (overrides other options)").action(async (opts) => {
1109
+ facts.command("create").description("Create a new fact definition").option("--key <key>", "Fact key (lowercase, underscores)").option("--name <n>", "Display name").option("--type <type>", "Value type: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER").option("--description <desc>", "Description").option("--required", "Mark as required", false).option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1110
+ "after",
1111
+ dedent6`
1112
+
1113
+ Examples:
1114
+ $ lexq facts create --key customer_tier --name "Customer Tier" --type STRING
1115
+ $ lexq facts create --key order_total --name "Order Total" --type NUMBER --required
1116
+
1117
+ $ lexq facts create --json '{
1118
+ "key": "user_region",
1119
+ "name": "User Region",
1120
+ "type": "STRING",
1121
+ "description": "ISO country code",
1122
+ "isRequired": false
1123
+ }'
1124
+
1125
+ Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1126
+ Key Format: lowercase letters, numbers, underscores only (e.g., payment_amount)
1127
+ `
1128
+ ).action(async (opts) => {
814
1129
  try {
815
1130
  const globalOpts = program.opts();
816
1131
  const body = opts.json ? JSON.parse(opts.json) : buildCreateBody2(opts);
@@ -827,7 +1142,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
827
1142
  process.exit(1);
828
1143
  }
829
1144
  });
830
- facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <name>", "Display name").option("--description <desc>", "Description").option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--json <body>", "Full request body as JSON (overrides other options)").action(async (opts) => {
1145
+ facts.command("update").description("Update a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--name <n>", "Display name").option("--description <desc>", "Description").option("--required", "Mark as required").option("--no-required", "Mark as not required").option("--json <body>", "Full request body as JSON (overrides other options)").addHelpText(
1146
+ "after",
1147
+ dedent6`
1148
+
1149
+ System facts cannot be modified. Only display name, description, and required flag can be changed.
1150
+
1151
+ Example:
1152
+ $ lexq facts update --id <factId> --name "Updated Name" --required
1153
+ `
1154
+ ).action(async (opts) => {
831
1155
  try {
832
1156
  const globalOpts = program.opts();
833
1157
  const body = opts.json ? JSON.parse(opts.json) : buildUpdateBody2(opts);
@@ -844,7 +1168,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
844
1168
  process.exit(1);
845
1169
  }
846
1170
  });
847
- facts.command("delete").description("Delete a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
1171
+ facts.command("delete").description("Delete a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--force", "Skip confirmation prompt").addHelpText(
1172
+ "after",
1173
+ dedent6`
1174
+
1175
+ System facts cannot be deleted. Use --force to skip the confirmation prompt.
1176
+ `
1177
+ ).action(async (opts) => {
848
1178
  try {
849
1179
  const globalOpts = program.opts();
850
1180
  if (!opts.force) {
@@ -869,7 +1199,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
869
1199
  process.exit(1);
870
1200
  }
871
1201
  });
872
- facts.command("action-metadata").description("Get action runtime fact metadata").action(async () => {
1202
+ facts.command("action-metadata").description("Get action runtime fact metadata").addHelpText(
1203
+ "after",
1204
+ dedent6`
1205
+
1206
+ Shows which facts are automatically created by each action type at runtime.
1207
+ Useful for understanding what output variables are available after rule execution.
1208
+ `
1209
+ ).action(async () => {
873
1210
  try {
874
1211
  const globalOpts = program.opts();
875
1212
  const data = await apiRequest("GET", "schema/action-metadata", {
@@ -886,9 +1223,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
886
1223
  });
887
1224
  }
888
1225
  function buildCreateBody2(opts) {
889
- if (!opts.key || !opts.name || !opts.type) {
1226
+ if (!opts.key || !opts.name || !opts.type)
890
1227
  throw new Error("--key, --name, and --type are required (or use --json).");
891
- }
892
1228
  const body = {
893
1229
  key: opts.key,
894
1230
  name: opts.name,
@@ -908,9 +1244,39 @@ function buildUpdateBody2(opts) {
908
1244
 
909
1245
  // src/commands/deploy.ts
910
1246
  import "commander";
1247
+ import dedent7 from "dedent";
911
1248
  function registerDeployCommands(program) {
912
- const deploy = program.command("deploy").description("Deployment lifecycle and history");
913
- deploy.command("publish").description("Publish a DRAFT version (DRAFT \u2192 ACTIVE)").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Version ID to publish").requiredOption("--memo <memo>", "Publish Deployment memo").action(async (opts) => {
1249
+ const deploy = program.command("deploy").description("Deployment lifecycle and history").addHelpText(
1250
+ "after",
1251
+ dedent7`
1252
+
1253
+ Lifecycle: Publish (DRAFT→ACTIVE) → Deploy (ACTIVE→LIVE) → Rollback / Undeploy
1254
+
1255
+ Commands:
1256
+ publish Lock a DRAFT version (DRAFT → ACTIVE)
1257
+ live Push an ACTIVE version to production traffic
1258
+ rollback Revert to the previous deployed version
1259
+ undeploy Remove the live version (stops all traffic)
1260
+ history List deployment history with filters
1261
+ detail Get deployment detail with integrity check
1262
+ overview Show all groups' deployment status at a glance
1263
+ deployable List ACTIVE versions available for deployment
1264
+ diff Compare rule snapshots between two versions
1265
+
1266
+ Always dry-run before publishing. Cannot deploy a DRAFT — publish first.
1267
+ `
1268
+ );
1269
+ deploy.command("publish").description("Publish a DRAFT version (DRAFT \u2192 ACTIVE)").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Version ID to publish").requiredOption("--memo <memo>", "Publish Deployment memo").addHelpText(
1270
+ "after",
1271
+ dedent7`
1272
+
1273
+ Locks the version permanently. Rules cannot be modified after publishing.
1274
+ A snapshot hash is generated for integrity verification.
1275
+
1276
+ Example:
1277
+ $ lexq deploy publish --group-id <gid> --version-id <vid> --memo "Validated via dry-run"
1278
+ `
1279
+ ).action(async (opts) => {
914
1280
  try {
915
1281
  const globalOpts = program.opts();
916
1282
  await apiRequest(
@@ -930,7 +1296,16 @@ function registerDeployCommands(program) {
930
1296
  process.exit(1);
931
1297
  }
932
1298
  });
933
- deploy.command("live").description("Deploy an ACTIVE version to live traffic").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Version ID to deploy").requiredOption("--memo <memo>", "Live Deployment memo").action(async (opts) => {
1299
+ deploy.command("live").description("Deploy an ACTIVE version to live traffic").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Version ID to deploy").requiredOption("--memo <memo>", "Live Deployment memo").addHelpText(
1300
+ "after",
1301
+ dedent7`
1302
+
1303
+ Takes effect immediately. The version starts receiving production traffic.
1304
+
1305
+ Example:
1306
+ $ lexq deploy live --group-id <gid> --version-id <vid> --memo "Go live — v3"
1307
+ `
1308
+ ).action(async (opts) => {
934
1309
  try {
935
1310
  const globalOpts = program.opts();
936
1311
  await apiRequest("POST", `policy-groups/${opts.groupId}/deploy`, {
@@ -946,7 +1321,17 @@ function registerDeployCommands(program) {
946
1321
  process.exit(1);
947
1322
  }
948
1323
  });
949
- deploy.command("rollback").description("Rollback to the previous deployed version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Rollback reason").option("--force", "Skip confirmation prompt").action(async (opts) => {
1324
+ deploy.command("rollback").description("Rollback to the previous deployed version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Rollback reason").option("--force", "Skip confirmation prompt").addHelpText(
1325
+ "after",
1326
+ dedent7`
1327
+
1328
+ Reverts to the version that was live before the current one.
1329
+ Only available if the previous version is still ACTIVE.
1330
+
1331
+ Example:
1332
+ $ lexq deploy rollback --group-id <gid> --memo "High error rate" --force
1333
+ `
1334
+ ).action(async (opts) => {
950
1335
  try {
951
1336
  const globalOpts = program.opts();
952
1337
  if (!opts.force) {
@@ -972,7 +1357,16 @@ function registerDeployCommands(program) {
972
1357
  process.exit(1);
973
1358
  }
974
1359
  });
975
- deploy.command("undeploy").description("Remove the live version from a group").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Undeploy reason").option("--force", "Skip confirmation prompt").action(async (opts) => {
1360
+ deploy.command("undeploy").description("Remove the live version from a group").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--memo <memo>", "Undeploy reason").option("--force", "Skip confirmation prompt").addHelpText(
1361
+ "after",
1362
+ dedent7`
1363
+
1364
+ Stops all traffic processing for this group until a new version is deployed.
1365
+
1366
+ Example:
1367
+ $ lexq deploy undeploy --group-id <gid> --memo "Maintenance window" --force
1368
+ `
1369
+ ).action(async (opts) => {
976
1370
  try {
977
1371
  const globalOpts = program.opts();
978
1372
  if (!opts.force) {
@@ -1001,14 +1395,18 @@ function registerDeployCommands(program) {
1001
1395
  deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option(
1002
1396
  "--types <types>",
1003
1397
  "Filter by types (comma-separated: PUBLISH,DEPLOY,ROLLBACK,UNDEPLOY)"
1004
- ).option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
1398
+ ).option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
1399
+ "after",
1400
+ dedent7`
1401
+
1402
+ Example:
1403
+ $ lexq deploy history --group-id <gid> --types DEPLOY,ROLLBACK --format table
1404
+ `
1405
+ ).action(async (opts) => {
1005
1406
  try {
1006
1407
  const globalOpts = program.opts();
1007
1408
  const format = globalOpts.format ?? "json";
1008
- const params = {
1009
- page: opts.page,
1010
- size: opts.size
1011
- };
1409
+ const params = { page: opts.page, size: opts.size };
1012
1410
  if (opts.groupId) params.groupId = opts.groupId;
1013
1411
  if (opts.types) params.types = opts.types;
1014
1412
  if (opts.startDate) params.startDate = opts.startDate;
@@ -1043,7 +1441,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1043
1441
  process.exit(1);
1044
1442
  }
1045
1443
  });
1046
- deploy.command("detail").description("Get deployment detail").requiredOption("--id <deploymentId>", "Deployment ID").action(async (opts) => {
1444
+ deploy.command("detail").description("Get deployment detail").requiredOption("--id <deploymentId>", "Deployment ID").addHelpText(
1445
+ "after",
1446
+ dedent7`
1447
+
1448
+ Includes snapshot hash and integrity check (hashValid field).
1449
+ `
1450
+ ).action(async (opts) => {
1047
1451
  try {
1048
1452
  const globalOpts = program.opts();
1049
1453
  const data = await apiRequest("GET", `deployments/${opts.id}`, {
@@ -1058,7 +1462,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1058
1462
  process.exit(1);
1059
1463
  }
1060
1464
  });
1061
- deploy.command("overview").description("Show deployment status overview for all groups").action(async () => {
1465
+ deploy.command("overview").description("Show deployment status overview for all groups").addHelpText(
1466
+ "after",
1467
+ dedent7`
1468
+
1469
+ Shows which version is live for each group, who deployed it, and when.
1470
+
1471
+ Example:
1472
+ $ lexq deploy overview --format table
1473
+ `
1474
+ ).action(async () => {
1062
1475
  try {
1063
1476
  const globalOpts = program.opts();
1064
1477
  const format = globalOpts.format ?? "json";
@@ -1087,7 +1500,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1087
1500
  process.exit(1);
1088
1501
  }
1089
1502
  });
1090
- deploy.command("deployable").description("List deployable (ACTIVE) versions for a group").requiredOption("--group-id <groupId>", "Policy group ID").action(async (opts) => {
1503
+ deploy.command("deployable").description("List deployable (ACTIVE) versions for a group").requiredOption("--group-id <groupId>", "Policy group ID").addHelpText(
1504
+ "after",
1505
+ dedent7`
1506
+
1507
+ Shows ACTIVE versions that can be deployed. Only published versions appear here.
1508
+ `
1509
+ ).action(async (opts) => {
1091
1510
  try {
1092
1511
  const globalOpts = program.opts();
1093
1512
  const data = await apiRequest(
@@ -1106,7 +1525,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1106
1525
  process.exit(1);
1107
1526
  }
1108
1527
  });
1109
- deploy.command("diff").description("Compare snapshot diff between two versions").requiredOption("--base <versionId>", "Base version ID").requiredOption("--target <versionId>", "Target version ID").action(async (opts) => {
1528
+ deploy.command("diff").description("Compare snapshot diff between two versions").requiredOption("--base <versionId>", "Base version ID").requiredOption("--target <versionId>", "Target version ID").addHelpText(
1529
+ "after",
1530
+ dedent7`
1531
+
1532
+ Shows added, removed, and modified rules between two versions.
1533
+
1534
+ Example:
1535
+ $ lexq deploy diff --base <v1-id> --target <v2-id>
1536
+ `
1537
+ ).action(async (opts) => {
1110
1538
  try {
1111
1539
  const globalOpts = program.opts();
1112
1540
  const data = await apiRequest("GET", "deployments/diff", {
@@ -1114,10 +1542,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1114
1542
  baseUrl: globalOpts.baseUrl,
1115
1543
  dryRun: globalOpts.dryRun,
1116
1544
  verbose: globalOpts.verbose,
1117
- params: {
1118
- baseVersionId: opts.base,
1119
- targetVersionId: opts.target
1120
- }
1545
+ params: { baseVersionId: opts.base, targetVersionId: opts.target }
1121
1546
  });
1122
1547
  printJson(data);
1123
1548
  } catch (error) {
@@ -1130,9 +1555,38 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1130
1555
  // src/commands/analytics.ts
1131
1556
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1132
1557
  import "commander";
1558
+ import dedent8 from "dedent";
1133
1559
  function registerAnalyticsCommands(program) {
1134
- const analytics = program.command("analytics").description("Dry run, simulation, and requirements");
1135
- analytics.command("dry-run").description("Execute a single dry run against a version").requiredOption("--version-id <versionId>", "Policy version ID").option("--json <body>", "Request body as JSON string").option("--file <path>", "Read request body from a JSON file").option("--debug", "Include debug traces", false).option("--mock", "Mock external calls", false).action(async (opts) => {
1560
+ const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
1561
+ "after",
1562
+ dedent8`
1563
+
1564
+ Test and validate rules before deploying to production.
1565
+
1566
+ Commands:
1567
+ dry-run Test a single input against a version
1568
+ dry-run-compare Compare results between two versions
1569
+ requirements Show required input facts for a version
1570
+ simulation Batch test against historical data (start, status, list, cancel, export)
1571
+ dataset Upload datasets and download templates
1572
+
1573
+ Workflow: facts check → dry-run → publish → simulation → deploy
1574
+ `
1575
+ );
1576
+ analytics.command("dry-run").description("Execute a single dry run against a version").requiredOption("--version-id <versionId>", "Policy version ID").option("--json <body>", "Request body as JSON string").option("--file <path>", "Read request body from a JSON file").option("--debug", "Include debug traces", false).option("--mock", "Mock external calls", false).addHelpText(
1577
+ "after",
1578
+ dedent8`
1579
+
1580
+ Examples:
1581
+ $ lexq analytics dry-run --version-id <vid> --debug --mock \\
1582
+ --json '{"facts": {"payment_amount": 150000, "customer_tier": "VIP"}}'
1583
+
1584
+ $ lexq analytics dry-run --version-id <vid> --file test-input.json
1585
+
1586
+ The request body must include a "facts" object. Use --debug for execution traces
1587
+ and --mock to skip external service calls (webhooks, coupons, etc.).
1588
+ `
1589
+ ).action(async (opts) => {
1136
1590
  try {
1137
1591
  const globalOpts = program.opts();
1138
1592
  const body = resolveBody(opts);
@@ -1158,7 +1612,20 @@ function registerAnalyticsCommands(program) {
1158
1612
  process.exit(1);
1159
1613
  }
1160
1614
  });
1161
- analytics.command("dry-run-compare").description("Compare dry run results between two versions").option("--json <body>", "Request body as JSON string").option("--file <path>", "Read request body from a JSON file").action(async (opts) => {
1615
+ analytics.command("dry-run-compare").description("Compare dry run results between two versions").option("--json <body>", "Request body as JSON string").option("--file <path>", "Read request body from a JSON file").addHelpText(
1616
+ "after",
1617
+ dedent8`
1618
+
1619
+ Example:
1620
+ $ lexq analytics dry-run-compare --json '{
1621
+ "versionIdA": "<version-a-id>",
1622
+ "versionIdB": "<version-b-id>",
1623
+ "facts": {"payment_amount": 100000, "customer_tier": "VIP"}
1624
+ }'
1625
+
1626
+ Shows side-by-side which rules matched and what actions fired for each version.
1627
+ `
1628
+ ).action(async (opts) => {
1162
1629
  try {
1163
1630
  const globalOpts = program.opts();
1164
1631
  const body = resolveBody(opts);
@@ -1180,7 +1647,16 @@ function registerAnalyticsCommands(program) {
1180
1647
  process.exit(1);
1181
1648
  }
1182
1649
  });
1183
- analytics.command("requirements").description("Analyze required input facts for a version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").action(async (opts) => {
1650
+ analytics.command("requirements").description("Analyze required input facts for a version").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").addHelpText(
1651
+ "after",
1652
+ dedent8`
1653
+
1654
+ Shows all facts referenced in conditions and actions, along with an example request body.
1655
+
1656
+ Example:
1657
+ $ lexq analytics requirements --group-id <gid> --version-id <vid> --format table
1658
+ `
1659
+ ).action(async (opts) => {
1184
1660
  try {
1185
1661
  const globalOpts = program.opts();
1186
1662
  const format = globalOpts.format ?? "json";
@@ -1215,8 +1691,44 @@ function registerAnalyticsCommands(program) {
1215
1691
  process.exit(1);
1216
1692
  }
1217
1693
  });
1218
- const sim = analytics.command("simulation").description("Manage batch simulations");
1219
- sim.command("start").description("Start a new batch simulation").requiredOption("--json <body>", "Simulation request body as JSON").option("--file <path>", "Read request body from a JSON file").action(async (opts) => {
1694
+ const sim = analytics.command("simulation").description("Manage batch simulations").addHelpText(
1695
+ "after",
1696
+ dedent8`
1697
+
1698
+ Batch-test a version against historical data or uploaded datasets.
1699
+
1700
+ Commands:
1701
+ start Start a new simulation
1702
+ status Check progress and results
1703
+ list List simulation history
1704
+ cancel Cancel a running simulation
1705
+ export Export results as CSV or JSON
1706
+
1707
+ Simulations always mock external calls. Use --format table for summary view.
1708
+ `
1709
+ );
1710
+ sim.command("start").description("Start a new batch simulation").requiredOption("--json <body>", "Simulation request body as JSON").option("--file <path>", "Read request body from a JSON file").addHelpText(
1711
+ "after",
1712
+ dedent8`
1713
+
1714
+ Example:
1715
+ $ lexq analytics simulation start --json '{
1716
+ "policyVersionId": "<vid>",
1717
+ "dataset": {
1718
+ "type": "EXECUTION_LOG",
1719
+ "source": "RECENT",
1720
+ "maxRecords": 1000
1721
+ },
1722
+ "options": {
1723
+ "baselinePolicyVersionId": "<baseline-vid>",
1724
+ "includeRuleStats": true
1725
+ }
1726
+ }'
1727
+
1728
+ Dataset types: EXECUTION_LOG, MANUAL
1729
+ Dataset sources: RECENT, DATE_RANGE, MANUAL
1730
+ `
1731
+ ).action(async (opts) => {
1220
1732
  try {
1221
1733
  const globalOpts = program.opts();
1222
1734
  const body = resolveBody(opts);
@@ -1234,7 +1746,16 @@ function registerAnalyticsCommands(program) {
1234
1746
  process.exit(1);
1235
1747
  }
1236
1748
  });
1237
- sim.command("status").description("Get simulation status and results").requiredOption("--id <simulationId>", "Simulation ID").action(async (opts) => {
1749
+ sim.command("status").description("Get simulation status and results").requiredOption("--id <simulationId>", "Simulation ID").addHelpText(
1750
+ "after",
1751
+ dedent8`
1752
+
1753
+ Shows progress, match rate, metric comparison (if baseline set), and per-rule stats.
1754
+
1755
+ Example:
1756
+ $ lexq analytics simulation status --id <simId> --format table
1757
+ `
1758
+ ).action(async (opts) => {
1238
1759
  try {
1239
1760
  const globalOpts = program.opts();
1240
1761
  const format = globalOpts.format ?? "json";
@@ -1314,10 +1835,7 @@ function registerAnalyticsCommands(program) {
1314
1835
  try {
1315
1836
  const globalOpts = program.opts();
1316
1837
  const format = globalOpts.format ?? "json";
1317
- const params = {
1318
- page: opts.page,
1319
- size: opts.size
1320
- };
1838
+ const params = { page: opts.page, size: opts.size };
1321
1839
  if (opts.status) params.status = opts.status;
1322
1840
  if (opts.from) params.from = opts.from;
1323
1841
  if (opts.to) params.to = opts.to;
@@ -1356,7 +1874,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1356
1874
  process.exit(1);
1357
1875
  }
1358
1876
  });
1359
- sim.command("cancel").description("Cancel a running simulation").requiredOption("--id <simulationId>", "Simulation ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
1877
+ sim.command("cancel").description("Cancel a running simulation").requiredOption("--id <simulationId>", "Simulation ID").option("--force", "Skip confirmation prompt").addHelpText(
1878
+ "after",
1879
+ dedent8`
1880
+
1881
+ Only PENDING or RUNNING simulations can be cancelled.
1882
+ `
1883
+ ).action(async (opts) => {
1360
1884
  try {
1361
1885
  const globalOpts = program.opts();
1362
1886
  if (!opts.force) {
@@ -1381,7 +1905,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1381
1905
  process.exit(1);
1382
1906
  }
1383
1907
  });
1384
- 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").action(async (opts) => {
1908
+ 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(
1909
+ "after",
1910
+ dedent8`
1911
+
1912
+ Only COMPLETED simulations can be exported.
1913
+
1914
+ Examples:
1915
+ $ lexq analytics simulation export --id <simId> --format csv --output results.csv
1916
+ $ lexq analytics simulation export --id <simId> --format json
1917
+ `
1918
+ ).action(async (opts) => {
1385
1919
  try {
1386
1920
  const globalOpts = program.opts();
1387
1921
  const exportFormat = opts.format === "csv" ? "csv" : "json";
@@ -1408,8 +1942,26 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1408
1942
  process.exit(1);
1409
1943
  }
1410
1944
  });
1411
- const dataset = analytics.command("dataset").description("Upload datasets and download templates");
1412
- dataset.command("upload").description("Upload a CSV or JSON file as a simulation dataset").requiredOption("--file <path>", "Path to CSV or JSON file").action(async (opts) => {
1945
+ const dataset = analytics.command("dataset").description("Upload datasets and download templates").addHelpText(
1946
+ "after",
1947
+ dedent8`
1948
+
1949
+ Commands:
1950
+ upload Upload a CSV or JSON file as a simulation dataset
1951
+ template Download a dataset template based on version requirements
1952
+ `
1953
+ );
1954
+ dataset.command("upload").description("Upload a CSV or JSON file as a simulation dataset").requiredOption("--file <path>", "Path to CSV or JSON file").addHelpText(
1955
+ "after",
1956
+ dedent8`
1957
+
1958
+ Supported formats: CSV (with header row), JSON (array of objects).
1959
+ Use "dataset template" to generate a correctly formatted template.
1960
+
1961
+ Example:
1962
+ $ lexq analytics dataset upload --file transactions.csv
1963
+ `
1964
+ ).action(async (opts) => {
1413
1965
  try {
1414
1966
  const globalOpts = program.opts();
1415
1967
  const config = loadConfig();
@@ -1455,7 +2007,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1455
2007
  process.exit(1);
1456
2008
  }
1457
2009
  });
1458
- 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").action(async (opts) => {
2010
+ 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(
2011
+ "after",
2012
+ dedent8`
2013
+
2014
+ Generates a template with all required fact columns pre-filled.
2015
+
2016
+ Examples:
2017
+ $ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
2018
+ $ lexq analytics dataset template --group-id <gid> --version-id <vid> --format json
2019
+ `
2020
+ ).action(async (opts) => {
1459
2021
  try {
1460
2022
  const globalOpts = program.opts();
1461
2023
  const config = loadConfig();
@@ -1502,16 +2064,35 @@ function resolveBody(opts) {
1502
2064
 
1503
2065
  // src/commands/history.ts
1504
2066
  import "commander";
2067
+ import dedent9 from "dedent";
1505
2068
  function registerHistoryCommands(program) {
1506
- const history = program.command("history").description("Execution history");
1507
- history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>", "Filter by status (SUCCESS, NO_MATCH, ERROR, TIMEOUT)").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
2069
+ const history = program.command("history").description("Execution history").addHelpText(
2070
+ "after",
2071
+ dedent9`
2072
+
2073
+ View and analyze policy execution logs from production traffic.
2074
+
2075
+ Commands:
2076
+ list List execution history with filters
2077
+ get Get full execution detail (request facts, traces, decisions)
2078
+ stats Aggregate statistics (success rate, latency, counts)
2079
+
2080
+ Statuses: SUCCESS, NO_MATCH, ERROR, TIMEOUT
2081
+ `
2082
+ );
2083
+ history.command("list").description("List execution history").option("--trace-id <traceId>", "Filter by trace ID").option("--group-id <groupId>", "Filter by policy group").option("--version-id <versionId>", "Filter by version").option("--status <status>", "Filter by status (SUCCESS, NO_MATCH, ERROR, TIMEOUT)").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
2084
+ "after",
2085
+ dedent9`
2086
+
2087
+ Examples:
2088
+ $ lexq history list --status ERROR --format table
2089
+ $ lexq history list --group-id <gid> --start-date 2026-04-01 --end-date 2026-04-15
2090
+ `
2091
+ ).action(async (opts) => {
1508
2092
  try {
1509
2093
  const globalOpts = program.opts();
1510
2094
  const format = globalOpts.format ?? "json";
1511
- const params = {
1512
- page: opts.page,
1513
- size: opts.size
1514
- };
2095
+ const params = { page: opts.page, size: opts.size };
1515
2096
  if (opts.traceId) params.traceId = opts.traceId;
1516
2097
  if (opts.groupId) params.policyGroupId = opts.groupId;
1517
2098
  if (opts.versionId) params.versionId = opts.versionId;
@@ -1553,7 +2134,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1553
2134
  process.exit(1);
1554
2135
  }
1555
2136
  });
1556
- history.command("get").description("Get execution detail").requiredOption("--id <traceId>", "Trace ID").action(async (opts) => {
2137
+ history.command("get").description("Get execution detail").requiredOption("--id <traceId>", "Trace ID").addHelpText(
2138
+ "after",
2139
+ dedent9`
2140
+
2141
+ Returns the full execution detail including request facts, result traces,
2142
+ and decision traces (SELECTED, BLOCKED_MUTEX, LOST_PRIORITY, etc.).
2143
+ `
2144
+ ).action(async (opts) => {
1557
2145
  try {
1558
2146
  const globalOpts = program.opts();
1559
2147
  const data = await apiRequest(
@@ -1572,7 +2160,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1572
2160
  process.exit(1);
1573
2161
  }
1574
2162
  });
1575
- history.command("stats").description("Get execution statistics").option("--group-id <groupId>", "Filter by policy group").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").action(async (opts) => {
2163
+ history.command("stats").description("Get execution statistics").option("--group-id <groupId>", "Filter by policy group").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").addHelpText(
2164
+ "after",
2165
+ dedent9`
2166
+
2167
+ Shows total executions, success/no-match/failure counts, success rate, and avg latency.
2168
+
2169
+ Example:
2170
+ $ lexq history stats --format table
2171
+ $ lexq history stats --group-id <gid> --start-date 2026-04-01
2172
+ `
2173
+ ).action(async (opts) => {
1576
2174
  try {
1577
2175
  const globalOpts = program.opts();
1578
2176
  const format = globalOpts.format ?? "json";
@@ -1613,8 +2211,25 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1613
2211
 
1614
2212
  // src/commands/integrations.ts
1615
2213
  import "commander";
2214
+ import dedent10 from "dedent";
1616
2215
  function registerIntegrationCommands(program) {
1617
- const integrations = program.command("integrations").description("Manage external integrations");
2216
+ const integrations = program.command("integrations").description("Manage external integrations").addHelpText(
2217
+ "after",
2218
+ dedent10`
2219
+
2220
+ Integrations connect rule actions to external services (webhooks, coupons,
2221
+ points, notifications, CRM, messengers).
2222
+
2223
+ Commands:
2224
+ list List all integrations
2225
+ get Get integration detail
2226
+ save Create or update an integration
2227
+ delete Delete an integration
2228
+ config-spec Show required configuration fields per type
2229
+
2230
+ Types: COUPON, POINT, NOTIFICATION, CRM, MESSENGER, WEBHOOK
2231
+ `
2232
+ );
1618
2233
  integrations.command("list").description("List integrations").option(
1619
2234
  "--type <type>",
1620
2235
  "Filter by type (COUPON, POINT, NOTIFICATION, CRM, MESSENGER, WEBHOOK)"
@@ -1622,10 +2237,7 @@ function registerIntegrationCommands(program) {
1622
2237
  try {
1623
2238
  const globalOpts = program.opts();
1624
2239
  const format = globalOpts.format ?? "json";
1625
- const params = {
1626
- page: opts.page,
1627
- size: opts.size
1628
- };
2240
+ const params = { page: opts.page, size: opts.size };
1629
2241
  if (opts.type) params.type = opts.type;
1630
2242
  const data = await apiRequest("GET", "integrations", {
1631
2243
  apiKey: globalOpts.apiKey,
@@ -1671,7 +2283,40 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1671
2283
  process.exit(1);
1672
2284
  }
1673
2285
  });
1674
- integrations.command("save").description("Create or update an integration").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
2286
+ integrations.command("save").description("Create or update an integration").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
2287
+ "after",
2288
+ dedent10`
2289
+
2290
+ Examples:
2291
+ # Create
2292
+ $ lexq integrations save --json '{
2293
+ "type": "WEBHOOK",
2294
+ "name": "Order Processing",
2295
+ "baseUrl": "https://api.example.com/webhooks/orders",
2296
+ "isActive": true
2297
+ }'
2298
+
2299
+ # Update (provide id)
2300
+ $ lexq integrations save --json '{
2301
+ "id": "<existing-id>",
2302
+ "type": "WEBHOOK",
2303
+ "name": "Order Processing (v2)",
2304
+ "baseUrl": "https://api.example.com/v2/webhooks/orders",
2305
+ "isActive": true
2306
+ }'
2307
+
2308
+ Fields:
2309
+ id string Provide to update, omit to create
2310
+ type string COUPON | POINT | NOTIFICATION | CRM | MESSENGER | WEBHOOK (required)
2311
+ name string Integration name (required, unique per tenant)
2312
+ baseUrl string Base URL of the external service (required)
2313
+ credential string API key or token (optional, write-only)
2314
+ additionalConfig object Extra config key-value pairs (optional)
2315
+ isActive boolean Enable/disable [default: true]
2316
+
2317
+ Use "lexq integrations config-spec" to see required fields per type.
2318
+ `
2319
+ ).action(async (opts) => {
1675
2320
  try {
1676
2321
  const globalOpts = program.opts();
1677
2322
  const body = JSON.parse(opts.json);
@@ -1688,7 +2333,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1688
2333
  process.exit(1);
1689
2334
  }
1690
2335
  });
1691
- integrations.command("delete").description("Delete an integration").requiredOption("--id <integrationId>", "Integration ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
2336
+ integrations.command("delete").description("Delete an integration").requiredOption("--id <integrationId>", "Integration ID").option("--force", "Skip confirmation prompt").addHelpText(
2337
+ "after",
2338
+ dedent10`
2339
+
2340
+ Rules referencing this integration will fail at execution time.
2341
+ Use --force to skip confirmation.
2342
+ `
2343
+ ).action(async (opts) => {
1692
2344
  try {
1693
2345
  const globalOpts = program.opts();
1694
2346
  if (!opts.force) {
@@ -1713,7 +2365,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1713
2365
  process.exit(1);
1714
2366
  }
1715
2367
  });
1716
- integrations.command("config-spec").description("Get integration configuration field specs").action(async () => {
2368
+ integrations.command("config-spec").description("Get integration configuration field specs").addHelpText(
2369
+ "after",
2370
+ dedent10`
2371
+
2372
+ Shows required and optional configuration fields for each integration type.
2373
+
2374
+ Example:
2375
+ $ lexq integrations config-spec
2376
+ `
2377
+ ).action(async () => {
1717
2378
  try {
1718
2379
  const globalOpts = program.opts();
1719
2380
  const data = await apiRequest("GET", "integrations/config-spec", {
@@ -1732,6 +2393,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1732
2393
 
1733
2394
  // src/commands/logs.ts
1734
2395
  import "commander";
2396
+ import dedent11 from "dedent";
1735
2397
 
1736
2398
  // src/types/enums.ts
1737
2399
  var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
@@ -1750,20 +2412,50 @@ var TaskType = [
1750
2412
  "WEBHOOK_EXECUTE",
1751
2413
  // Internal
1752
2414
  "IMAGE_PROCESSING",
1753
- "DAILY_SETTLEMENT"
2415
+ "DAILY_SETTLEMENT",
2416
+ "PLATFORM_WEBHOOK"
2417
+ ];
2418
+ var PlatformEventType = [
2419
+ "VERSION_PUBLISHED",
2420
+ "DEPLOYED",
2421
+ "ROLLED_BACK",
2422
+ "UNDEPLOYED"
1754
2423
  ];
2424
+ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
1755
2425
 
1756
2426
  // src/commands/logs.ts
1757
2427
  function registerLogCommands(program) {
1758
- const logs = program.command("logs").description("Failure logs");
1759
- logs.command("list").description("List failure logs").option("--category <category>", "Filter by category (INTEGRATION, INTERNAL)").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", "Filter by status (PENDING, RESOLVED, IGNORED)").option("--keyword <keyword>", "Search keyword").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
2428
+ const logs = program.command("logs").description("Failure logs").addHelpText(
2429
+ "after",
2430
+ dedent11`
2431
+
2432
+ System failure logs (DLQ) for background tasks — webhook calls, coupon issuance,
2433
+ point operations, notifications, and platform event webhooks.
2434
+
2435
+ Commands:
2436
+ list List failure logs with filters
2437
+ get Get failure log detail (includes payload for retry)
2438
+ action Process a single log (RETRY, IGNORE, RESOLVE)
2439
+ bulk-action Process multiple logs at once
2440
+
2441
+ Statuses: PENDING (needs attention), RESOLVED, IGNORED
2442
+ Categories: INTEGRATION (external), INTERNAL (system)
2443
+ `
2444
+ );
2445
+ logs.command("list").description("List failure logs").option("--category <category>", "Filter by category (INTEGRATION, INTERNAL)").option("--task-type <taskType>", `Filter by task type (${TaskType.join(", ")})`).option("--status <status>", "Filter by status (PENDING, RESOLVED, IGNORED)").option("--keyword <keyword>", "Search keyword").option("--start-date <date>", "Start date (yyyy-MM-dd)").option("--end-date <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").addHelpText(
2446
+ "after",
2447
+ dedent11`
2448
+
2449
+ Examples:
2450
+ $ lexq logs list --status PENDING --format table
2451
+ $ lexq logs list --task-type PLATFORM_WEBHOOK --category INTERNAL
2452
+ $ lexq logs list --keyword "timeout" --start-date 2026-04-01
2453
+ `
2454
+ ).action(async (opts) => {
1760
2455
  try {
1761
2456
  const globalOpts = program.opts();
1762
2457
  const format = globalOpts.format ?? "json";
1763
- const params = {
1764
- page: opts.page,
1765
- size: opts.size
1766
- };
2458
+ const params = { page: opts.page, size: opts.size };
1767
2459
  if (opts.category) params.category = opts.category;
1768
2460
  if (opts.taskType) params.taskType = opts.taskType;
1769
2461
  if (opts.status) params.status = opts.status;
@@ -1801,7 +2493,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1801
2493
  process.exit(1);
1802
2494
  }
1803
2495
  });
1804
- logs.command("get").description("Get failure log detail").requiredOption("--id <logId>", "Log ID").action(async (opts) => {
2496
+ logs.command("get").description("Get failure log detail").requiredOption("--id <logId>", "Log ID").addHelpText(
2497
+ "after",
2498
+ dedent11`
2499
+
2500
+ Includes the full payload that was used for the failed operation.
2501
+ Use this to inspect what went wrong before deciding to RETRY or RESOLVE.
2502
+ `
2503
+ ).action(async (opts) => {
1805
2504
  try {
1806
2505
  const globalOpts = program.opts();
1807
2506
  const data = await apiRequest("GET", `failure-logs/${opts.id}`, {
@@ -1816,7 +2515,19 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1816
2515
  process.exit(1);
1817
2516
  }
1818
2517
  });
1819
- logs.command("action").description("Process a failure log action (RETRY, IGNORE, RESOLVE)").requiredOption("--id <logId>", "Log ID").requiredOption("--action <action>", "Action: RETRY, IGNORE, or RESOLVE").action(async (opts) => {
2518
+ logs.command("action").description("Process a failure log action (RETRY, IGNORE, RESOLVE)").requiredOption("--id <logId>", "Log ID").requiredOption("--action <action>", "Action: RETRY, IGNORE, or RESOLVE").addHelpText(
2519
+ "after",
2520
+ dedent11`
2521
+
2522
+ Actions:
2523
+ RETRY Re-execute the failed operation with the original payload
2524
+ IGNORE Mark as intentionally skipped (won't appear in PENDING)
2525
+ RESOLVE Mark as manually resolved (e.g., fixed via external system)
2526
+
2527
+ Example:
2528
+ $ lexq logs action --id <logId> --action RETRY
2529
+ `
2530
+ ).action(async (opts) => {
1820
2531
  try {
1821
2532
  const globalOpts = program.opts();
1822
2533
  const data = await apiRequest(
@@ -1836,7 +2547,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1836
2547
  process.exit(1);
1837
2548
  }
1838
2549
  });
1839
- logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>", "Action: RETRY, IGNORE, or RESOLVE").action(async (opts) => {
2550
+ logs.command("bulk-action").description("Bulk process failure logs").requiredOption("--ids <logIds>", "Comma-separated log IDs").requiredOption("--action <action>", "Action: RETRY, IGNORE, or RESOLVE").addHelpText(
2551
+ "after",
2552
+ dedent11`
2553
+
2554
+ Processes each log individually. Failures are skipped with a warning.
2555
+
2556
+ Example:
2557
+ $ lexq logs bulk-action --ids "id1,id2,id3" --action RESOLVE
2558
+ `
2559
+ ).action(async (opts) => {
1840
2560
  try {
1841
2561
  const globalOpts = program.opts();
1842
2562
  const logIds = opts.ids.split(",").map((id) => id.trim());
@@ -1855,8 +2575,212 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1855
2575
  });
1856
2576
  }
1857
2577
 
2578
+ // src/commands/webhook-subscriptions.ts
2579
+ import "commander";
2580
+ import dedent12 from "dedent";
2581
+ function registerWebhookSubscriptionCommands(program) {
2582
+ const webhooks = program.command("webhook-subscriptions").description("Manage platform event webhook subscriptions").addHelpText(
2583
+ "after",
2584
+ dedent12`
2585
+
2586
+ Receive notifications when deployment lifecycle events occur
2587
+ (publish, deploy, rollback, undeploy).
2588
+
2589
+ Commands:
2590
+ list List all webhook subscriptions
2591
+ get Get subscription detail
2592
+ save Create or update a subscription
2593
+ delete Delete a subscription
2594
+ test Send a test event to verify connectivity
2595
+
2596
+ This is separate from Integrations (rule action webhooks).
2597
+ Webhook subscriptions are for platform-level event notifications.
2598
+ `
2599
+ );
2600
+ webhooks.command("list").description("List webhook subscriptions").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
2601
+ try {
2602
+ const globalOpts = program.opts();
2603
+ const format = globalOpts.format ?? "json";
2604
+ const data = await apiRequest(
2605
+ "GET",
2606
+ "webhook-subscriptions",
2607
+ {
2608
+ apiKey: globalOpts.apiKey,
2609
+ baseUrl: globalOpts.baseUrl,
2610
+ dryRun: globalOpts.dryRun,
2611
+ verbose: globalOpts.verbose,
2612
+ params: { page: opts.page, size: opts.size }
2613
+ }
2614
+ );
2615
+ if (format === "table") {
2616
+ printTable(
2617
+ ["ID", "Name", "Events", "Format", "Active"],
2618
+ data.content.map((s) => [
2619
+ s.id.substring(0, 8),
2620
+ s.name,
2621
+ s.subscribedEvents.join(", "),
2622
+ s.payloadFormat,
2623
+ s.isActive ? "\u2713" : "\u2717"
2624
+ ]),
2625
+ { truncate: 32 }
2626
+ );
2627
+ console.log(`
2628
+ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2629
+ } else {
2630
+ printJson(data);
2631
+ }
2632
+ } catch (error) {
2633
+ printError(error);
2634
+ process.exit(1);
2635
+ }
2636
+ });
2637
+ webhooks.command("get").description("Get webhook subscription detail").requiredOption("--id <subscriptionId>", "Subscription ID").action(async (opts) => {
2638
+ try {
2639
+ const globalOpts = program.opts();
2640
+ const data = await apiRequest(
2641
+ "GET",
2642
+ `webhook-subscriptions/${opts.id}`,
2643
+ {
2644
+ apiKey: globalOpts.apiKey,
2645
+ baseUrl: globalOpts.baseUrl,
2646
+ dryRun: globalOpts.dryRun,
2647
+ verbose: globalOpts.verbose
2648
+ }
2649
+ );
2650
+ printJson(data);
2651
+ } catch (error) {
2652
+ printError(error);
2653
+ process.exit(1);
2654
+ }
2655
+ });
2656
+ webhooks.command("save").description("Create or update a webhook subscription").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
2657
+ "after",
2658
+ dedent12`
2659
+
2660
+ Examples:
2661
+ # Create (Slack format)
2662
+ $ lexq webhook-subscriptions save --json '{
2663
+ "name": "Deploy Alert",
2664
+ "webhookUrl": "https://hooks.slack.com/services/...",
2665
+ "subscribedEvents": ["DEPLOYED", "ROLLED_BACK"],
2666
+ "payloadFormat": "SLACK"
2667
+ }'
2668
+
2669
+ # Update (provide id)
2670
+ $ lexq webhook-subscriptions save --json '{
2671
+ "id": "<existing-id>",
2672
+ "name": "Deploy Alert",
2673
+ "webhookUrl": "https://hooks.slack.com/services/...",
2674
+ "subscribedEvents": ["VERSION_PUBLISHED", "DEPLOYED", "ROLLED_BACK", "UNDEPLOYED"],
2675
+ "payloadFormat": "SLACK",
2676
+ "secret": "my-hmac-secret"
2677
+ }'
2678
+
2679
+ Fields:
2680
+ name string Subscription name (required, unique per tenant)
2681
+ webhookUrl string Webhook endpoint URL (required)
2682
+ subscribedEvents string[] VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED
2683
+ payloadFormat string GENERIC (full JSON) or SLACK ({"text":"..."}) [default: GENERIC]
2684
+ secret string HMAC-SHA256 signing secret (optional)
2685
+ isActive boolean Enable/disable (optional) [default: true]
2686
+ id string Provide to update, omit to create
2687
+
2688
+ When secret is set, an X-LexQ-Signature header (sha256=hex) is sent for verification.
2689
+ `
2690
+ ).action(async (opts) => {
2691
+ try {
2692
+ const globalOpts = program.opts();
2693
+ const body = JSON.parse(opts.json);
2694
+ const data = await apiRequest(
2695
+ "POST",
2696
+ "webhook-subscriptions",
2697
+ {
2698
+ apiKey: globalOpts.apiKey,
2699
+ baseUrl: globalOpts.baseUrl,
2700
+ dryRun: globalOpts.dryRun,
2701
+ verbose: globalOpts.verbose,
2702
+ body
2703
+ }
2704
+ );
2705
+ printJson(data);
2706
+ } catch (error) {
2707
+ printError(error);
2708
+ process.exit(1);
2709
+ }
2710
+ });
2711
+ webhooks.command("delete").description("Delete a webhook subscription").requiredOption("--id <subscriptionId>", "Subscription ID").option("--force", "Skip confirmation prompt").addHelpText(
2712
+ "after",
2713
+ dedent12`
2714
+
2715
+ Use --force to skip the confirmation prompt.
2716
+
2717
+ Example:
2718
+ $ lexq webhook-subscriptions delete --id <id> --force
2719
+ `
2720
+ ).action(async (opts) => {
2721
+ try {
2722
+ const globalOpts = program.opts();
2723
+ if (!opts.force) {
2724
+ const { createInterface: createInterface2 } = await import("readline/promises");
2725
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2726
+ const answer = await rl.question(`Delete webhook subscription ${opts.id}? [y/N] `);
2727
+ rl.close();
2728
+ if (answer.toLowerCase() !== "y") {
2729
+ console.log("Cancelled.");
2730
+ return;
2731
+ }
2732
+ }
2733
+ await apiRequest("DELETE", `webhook-subscriptions/${opts.id}`, {
2734
+ apiKey: globalOpts.apiKey,
2735
+ baseUrl: globalOpts.baseUrl,
2736
+ dryRun: globalOpts.dryRun,
2737
+ verbose: globalOpts.verbose
2738
+ });
2739
+ console.log(`\u2713 Webhook subscription ${opts.id} deleted.`);
2740
+ } catch (error) {
2741
+ printError(error);
2742
+ process.exit(1);
2743
+ }
2744
+ });
2745
+ webhooks.command("test").description("Send a test event to verify webhook connectivity").requiredOption("--id <subscriptionId>", "Subscription ID").addHelpText(
2746
+ "after",
2747
+ dedent12`
2748
+
2749
+ Sends a test event to the webhook URL and reports the HTTP status code.
2750
+ Does not record failures in the failure log.
2751
+
2752
+ The response includes:
2753
+ statusCode HTTP status code returned by the webhook endpoint
2754
+ success true if 2xx response received
2755
+ message human-readable status message
2756
+
2757
+ Example:
2758
+ $ lexq webhook-subscriptions test --id <id>
2759
+ `
2760
+ ).action(async (opts) => {
2761
+ try {
2762
+ const globalOpts = program.opts();
2763
+ const data = await apiRequest(
2764
+ "POST",
2765
+ `webhook-subscriptions/${opts.id}/test`,
2766
+ {
2767
+ apiKey: globalOpts.apiKey,
2768
+ baseUrl: globalOpts.baseUrl,
2769
+ dryRun: globalOpts.dryRun,
2770
+ verbose: globalOpts.verbose
2771
+ }
2772
+ );
2773
+ printJson(data);
2774
+ } catch (error) {
2775
+ printError(error);
2776
+ process.exit(1);
2777
+ }
2778
+ });
2779
+ }
2780
+
1858
2781
  // src/commands/serve.ts
1859
2782
  import "commander";
2783
+ import dedent15 from "dedent";
1860
2784
 
1861
2785
  // src/mcp/server.ts
1862
2786
  import { readFileSync as readFileSync3 } from "fs";
@@ -1996,7 +2920,7 @@ function registerGroupTools(server, callApi) {
1996
2920
  "lexq_groups_update",
1997
2921
  {
1998
2922
  title: "Update Policy Group",
1999
- description: "Update a policy group. This is a full replacement \u2014 omitted optional fields will be set to null on the server.",
2923
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
2000
2924
  inputSchema: {
2001
2925
  groupId: z.string().uuid().describe("Policy group ID"),
2002
2926
  name: z.string().optional().describe("New name"),
@@ -2093,10 +3017,10 @@ function registerVersionTools(server, callApi) {
2093
3017
  "lexq_versions_create",
2094
3018
  {
2095
3019
  title: "Create Policy Version",
2096
- description: "Create a new DRAFT version in a policy group. Provide a commit message and optional effective date range.",
3020
+ description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
2097
3021
  inputSchema: {
2098
3022
  groupId: z2.string().uuid().describe("Policy group ID"),
2099
- commitMessage: z2.string().describe("Commit message describing this version"),
3023
+ commitMessage: z2.string().optional().describe("Commit message describing this version"),
2100
3024
  effectiveFrom: z2.string().optional().describe("Effective start date (ISO 8601)"),
2101
3025
  effectiveTo: z2.string().optional().describe("Effective end date (ISO 8601)")
2102
3026
  }
@@ -2107,7 +3031,7 @@ function registerVersionTools(server, callApi) {
2107
3031
  "lexq_versions_update",
2108
3032
  {
2109
3033
  title: "Update Policy Version",
2110
- description: "Update a DRAFT version. Only DRAFT versions can be modified.",
3034
+ description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
2111
3035
  inputSchema: {
2112
3036
  groupId: z2.string().uuid().describe("Policy group ID"),
2113
3037
  versionId: z2.string().uuid().describe("Version ID"),
@@ -2150,6 +3074,7 @@ function registerVersionTools(server, callApi) {
2150
3074
 
2151
3075
  // src/mcp/tools/rules.ts
2152
3076
  import { z as z3 } from "zod";
3077
+ import dedent13 from "dedent";
2153
3078
  function registerRuleTools(server, callApi) {
2154
3079
  server.registerTool(
2155
3080
  "lexq_rules_list",
@@ -2184,32 +3109,34 @@ function registerRuleTools(server, callApi) {
2184
3109
  "lexq_rules_create",
2185
3110
  {
2186
3111
  title: "Create Rule",
2187
- description: `Create a rule in a DRAFT version. Requires name, priority, condition tree, and actions array.
2188
-
2189
- Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
2190
- If a required key is missing, ask the user to confirm the type, isRequired, and description
2191
- before calling lexq_facts_create \u2014 registering facts enables type validation, Console UI
2192
- autocomplete, and the dry-run requirements analyzer.
2193
-
2194
- Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
2195
- Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
2196
- Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
2197
-
2198
- Actions: [{ type, parameters }]
2199
-
2200
- Action parameter schemas:
2201
- - DISCOUNT: { refVar: string, method: "PERCENTAGE"|"AMOUNT", rate?: number (when PERCENTAGE), value?: number (when AMOUNT) }
2202
- - POINT: { refVar: string, targetVar: string, method: "PERCENTAGE"|"AMOUNT", rate?: number (when PERCENTAGE), value?: number (when AMOUNT), integrationId: uuid }
2203
- - COUPON_ISSUE: { couponId: string, integrationId: uuid }
2204
- - BLOCK: { reason: string }
2205
- - NOTIFICATION: { channel: "SMS"|"EMAIL"|"PUSH", targetVar: string, templateId: string, integrationId: uuid }
2206
- - WEBHOOK: { url: string, method: "POST", payloadTemplate?: object } payloadTemplate is optional. Without it, all facts are sent as-is. With it, the object is sent as the HTTP body with {{variables}} replaced at execution time. Variables: {{fact.xxx}}, {{output.xxx}}, {{timestamp}}, {{ruleName}}, {{groupName}}, {{versionNo}}, {{xxx}} (shorthand).
2207
- Platform examples:
2208
- Slack: { "text": "Rule {{ruleName}} fired \u2014 {{fact.customer_tier}}" }
2209
- Discord: { "content": "Rule {{ruleName}} fired \u2014 {{fact.customer_tier}}" }
2210
- Generic: { "event": "rule_matched", "rule": "{{ruleName}}", "amount": "{{output.payment_amount}}" }
2211
- - SET_FACT: { key: string, value: string|number|boolean }
2212
- - ADD_TAG: { tag: string, targetVar: string }`,
3112
+ description: dedent13`
3113
+ Create a rule in a DRAFT version. Requires name, priority, condition tree, and actions array.
3114
+
3115
+ Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
3116
+ If a required key is missing, ask the user to confirm the type, isRequired, and description
3117
+ before calling lexq_facts_create registering facts enables type validation, Console UI
3118
+ autocomplete, and the dry-run requirements analyzer.
3119
+
3120
+ Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
3121
+ Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
3122
+ Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
3123
+
3124
+ Actions: [{ type, parameters }]
3125
+
3126
+ Action parameter schemas:
3127
+ - DISCOUNT: { refVar: string, method: "PERCENTAGE"|"AMOUNT", rate?: number (when PERCENTAGE), value?: number (when AMOUNT) }
3128
+ - POINT: { refVar: string, targetVar: string, method: "PERCENTAGE"|"AMOUNT", rate?: number (when PERCENTAGE), value?: number (when AMOUNT), integrationId: uuid }
3129
+ - COUPON_ISSUE: { couponId: string, integrationId: uuid }
3130
+ - BLOCK: { reason: string }
3131
+ - NOTIFICATION: { channel: "SMS"|"EMAIL"|"PUSH", targetVar: string, templateId: string, integrationId: uuid }
3132
+ - WEBHOOK: { url: string, method: "POST", payloadTemplate?: object } payloadTemplate is optional. Without it, all facts are sent as-is. With it, the object is sent as the HTTP body with {{variables}} replaced at execution time. Variables: {{fact.xxx}}, {{output.xxx}}, {{timestamp}}, {{ruleName}}, {{groupName}}, {{versionNo}}, {{xxx}} (shorthand).
3133
+ Platform examples:
3134
+ Slack: { "text": "Rule {{ruleName}} fired {{fact.customer_tier}}" }
3135
+ Discord: { "content": "Rule {{ruleName}} fired {{fact.customer_tier}}" }
3136
+ Generic: { "event": "rule_matched", "rule": "{{ruleName}}", "amount": "{{output.payment_amount}}" }
3137
+ - SET_FACT: { key: string, value: string|number|boolean }
3138
+ - ADD_TAG: { tag: string, targetVar: string }
3139
+ `,
2213
3140
  inputSchema: {
2214
3141
  groupId: z3.string().uuid().describe("Policy group ID"),
2215
3142
  versionId: z3.string().uuid().describe("Version ID"),
@@ -2342,12 +3269,12 @@ function registerFactTools(server, callApi) {
2342
3269
  "lexq_facts_update",
2343
3270
  {
2344
3271
  title: "Update Fact Definition",
2345
- description: "Update a fact definition. Key and type cannot be changed. System facts only allow name and description changes.",
3272
+ description: "Update a fact definition. Key and type cannot be changed. Only provided fields are updated. System facts only allow name and description changes.",
2346
3273
  inputSchema: {
2347
3274
  factId: z4.string().uuid().describe("Fact definition ID"),
2348
- name: z4.string().describe("Display name"),
3275
+ name: z4.string().optional().describe("Display name"),
2349
3276
  description: z4.string().optional().describe("Description"),
2350
- isRequired: z4.boolean().describe("Required flag")
3277
+ isRequired: z4.boolean().optional().describe("Required flag")
2351
3278
  }
2352
3279
  },
2353
3280
  async ({ factId, ...body }) => callApi("PUT", `schema/facts/${factId}`, { body })
@@ -2363,6 +3290,15 @@ function registerFactTools(server, callApi) {
2363
3290
  },
2364
3291
  async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
2365
3292
  );
3293
+ server.registerTool(
3294
+ "lexq_facts_action_metadata",
3295
+ {
3296
+ title: "Get Action Runtime Fact Metadata",
3297
+ description: "Retrieve runtime Facts metadata for each Action type. Shows which Facts are required as input, produced as output, or consumed at runtime by each Action (DISCOUNT, SEND_SMS, ADD_TAG, SET_FACT, etc.). Use this BEFORE designing rules to understand which Action produces which output variables (e.g., DISCOUNT produces last_discount_amount). Data is static and changes only on engine deployment \u2014 safe to cache in-session.",
3298
+ inputSchema: {}
3299
+ },
3300
+ async () => callApi("GET", "schema/action-metadata")
3301
+ );
2366
3302
  }
2367
3303
 
2368
3304
  // src/mcp/tools/deploy.ts
@@ -2376,7 +3312,7 @@ function registerDeployTools(server, callApi) {
2376
3312
  inputSchema: {
2377
3313
  groupId: z5.string().uuid().describe("Policy group ID"),
2378
3314
  versionId: z5.string().uuid().describe("Version ID to publish"),
2379
- memo: z5.string().min(1).describe("Publish Deployment memo (required)")
3315
+ memo: z5.string().min(1).describe("Publish memo (required)")
2380
3316
  }
2381
3317
  },
2382
3318
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
@@ -2389,7 +3325,7 @@ function registerDeployTools(server, callApi) {
2389
3325
  inputSchema: {
2390
3326
  groupId: z5.string().uuid().describe("Policy group ID"),
2391
3327
  versionId: z5.string().uuid().describe("Version ID to deploy"),
2392
- memo: z5.string().min(1).describe("Live Deployment memo (required)")
3328
+ memo: z5.string().min(1).describe("Deployment memo (required)")
2393
3329
  }
2394
3330
  },
2395
3331
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
@@ -2403,7 +3339,7 @@ function registerDeployTools(server, callApi) {
2403
3339
  description: "Rollback to the previous deployed version. Only available if there is a previous version.",
2404
3340
  inputSchema: {
2405
3341
  groupId: z5.string().uuid().describe("Policy group ID"),
2406
- memo: z5.string().default("").describe("Rollback reason")
3342
+ memo: z5.string().min(1).describe("Rollback reason (required)")
2407
3343
  }
2408
3344
  },
2409
3345
  async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
@@ -2417,7 +3353,7 @@ function registerDeployTools(server, callApi) {
2417
3353
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
2418
3354
  inputSchema: {
2419
3355
  groupId: z5.string().uuid().describe("Policy group ID"),
2420
- memo: z5.string().default("").describe("Undeploy reason")
3356
+ memo: z5.string().min(1).describe("Undeploy reason (required)")
2421
3357
  }
2422
3358
  },
2423
3359
  async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
@@ -2433,13 +3369,19 @@ function registerDeployTools(server, callApi) {
2433
3369
  page: z5.number().int().min(0).default(0).describe("Page number"),
2434
3370
  size: z5.number().int().min(1).max(100).default(20).describe("Page size"),
2435
3371
  groupId: z5.string().uuid().optional().describe("Filter by group ID"),
2436
- type: z5.enum(["PUBLISH", "DEPLOY", "ROLLBACK", "UNDEPLOY"]).optional().describe("Filter by deployment type")
3372
+ types: z5.string().optional().describe(
3373
+ "Filter by deployment types (comma-separated: PUBLISH,DEPLOY,ROLLBACK,UNDEPLOY)"
3374
+ ),
3375
+ startDate: z5.string().optional().describe("Start date (yyyy-MM-dd)"),
3376
+ endDate: z5.string().optional().describe("End date (yyyy-MM-dd)")
2437
3377
  }
2438
3378
  },
2439
- async ({ page, size, groupId, type }) => {
3379
+ async ({ page, size, groupId, types, startDate, endDate }) => {
2440
3380
  const params = paginationParams(page, size);
2441
3381
  if (groupId) params.groupId = groupId;
2442
- if (type) params.type = type;
3382
+ if (types) params.types = types;
3383
+ if (startDate) params.startDate = startDate;
3384
+ if (endDate) params.endDate = endDate;
2443
3385
  return callApi("GET", "deployments", { params });
2444
3386
  }
2445
3387
  );
@@ -2463,18 +3405,53 @@ function registerDeployTools(server, callApi) {
2463
3405
  },
2464
3406
  async () => callApi("GET", "deployments/overview")
2465
3407
  );
3408
+ server.registerTool(
3409
+ "lexq_deploy_deployable",
3410
+ {
3411
+ title: "List Deployable Versions",
3412
+ description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
3413
+ inputSchema: {
3414
+ groupId: z5.string().uuid().describe("Policy group ID")
3415
+ }
3416
+ },
3417
+ async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
3418
+ );
3419
+ server.registerTool(
3420
+ "lexq_deploy_diff",
3421
+ {
3422
+ title: "Deployment Diff",
3423
+ description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
3424
+ inputSchema: {
3425
+ baseVersionId: z5.string().uuid().describe("Base version ID (typically the current live)"),
3426
+ targetVersionId: z5.string().uuid().describe("Target version ID (the one you want to deploy)")
3427
+ }
3428
+ },
3429
+ async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
3430
+ params: { baseVersionId, targetVersionId }
3431
+ })
3432
+ );
2466
3433
  }
2467
3434
 
2468
3435
  // src/mcp/tools/analytics.ts
2469
3436
  import { z as z6 } from "zod";
3437
+ import dedent14 from "dedent";
2470
3438
  function registerAnalyticsTools(server, callApi) {
2471
3439
  server.registerTool(
2472
3440
  "lexq_dry_run",
2473
3441
  {
2474
3442
  title: "Dry Run",
2475
- description: `Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
2476
- Example input: { "facts": { "payment_amount": 100000, "customer_tier": "VIP" } }
2477
- Always dry-run before publishing to validate rule behavior.`,
3443
+ description: dedent14`
3444
+ Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
3445
+
3446
+ Returns:
3447
+ inputFacts — normalized input facts
3448
+ mutatedFacts — input facts changed by rule actions (e.g. DISCOUNT mutates payment_amount)
3449
+ generatedVariables — new variables created by rules (e.g. last_discount_amount)
3450
+ executionTraces — per-rule match status
3451
+ decisionTraces — per-rule decision (SELECTED / BLOCKED_MUTEX / etc.)
3452
+
3453
+ Example input: { "facts": { "payment_amount": 100000, "customer_tier": "VIP" } }
3454
+ Always dry-run before publishing to validate rule behavior.`,
2478
3455
  inputSchema: {
2479
3456
  versionId: z6.string().uuid().describe("Policy version ID to test against"),
2480
3457
  facts: z6.string().describe('JSON string of facts object, e.g. {"payment_amount":100000}'),
@@ -2493,7 +3470,13 @@ function registerAnalyticsTools(server, callApi) {
2493
3470
  "lexq_dry_run_compare",
2494
3471
  {
2495
3472
  title: "Dry Run Compare",
2496
- description: "Compare dry run results between two versions using the same input facts. Useful for validating changes.",
3473
+ description: dedent14`
3474
+ Compare dry run results between two versions using the same input facts. Useful for validating changes.
3475
+
3476
+ Returns:
3477
+ resultA / resultB — full DryRunResponse for each version
3478
+ diff.mutatedDiff — changes in mutatedFacts between A and B (key → {before, after})
3479
+ diff.generatedDiff — changes in generatedVariables between A and B`,
2497
3480
  inputSchema: {
2498
3481
  versionIdA: z6.string().uuid().describe("Baseline version ID"),
2499
3482
  versionIdB: z6.string().uuid().describe("Candidate version ID"),
@@ -2523,21 +3506,23 @@ function registerAnalyticsTools(server, callApi) {
2523
3506
  "lexq_simulation_start",
2524
3507
  {
2525
3508
  title: "Start Simulation",
2526
- description: `Start a batch simulation against historical or uploaded data.
2527
-
2528
- dataset.type: "HISTORICAL" or "UPLOADED"
2529
- dataset.source (when HISTORICAL): "EXECUTION_LOGS"
2530
- dataset.from / dataset.to: date range (yyyy-MM-dd, when HISTORICAL)
2531
- options.maxRecords: number (max 100000, default 10000)
2532
- options.baselinePolicyVersionId: uuid (optional, for comparison)
2533
- options.includeRuleStats: boolean
2534
-
2535
- Example body:
2536
- {
2537
- "policyVersionId": "<uuid>",
2538
- "dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2025-01-01", "to": "2025-01-31" },
2539
- "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
2540
- }`,
3509
+ description: dedent14`
3510
+ Start a batch simulation against historical or uploaded data.
3511
+
3512
+ dataset.type: "HISTORICAL" or "UPLOADED"
3513
+ dataset.source (when HISTORICAL): "EXECUTION_LOGS"
3514
+ dataset.from / dataset.to: date range (yyyy-MM-dd, when HISTORICAL)
3515
+ options.maxRecords: number (max 100000, default 10000)
3516
+ options.baselinePolicyVersionId: uuid (optional, for comparison)
3517
+ options.includeRuleStats: boolean
3518
+
3519
+ Example body:
3520
+ {
3521
+ "policyVersionId": "<uuid>",
3522
+ "dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2025-01-01", "to": "2025-01-31" },
3523
+ "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
3524
+ }
3525
+ `,
2541
3526
  inputSchema: {
2542
3527
  body: z6.string().describe("JSON string of SimulationRequest")
2543
3528
  }
@@ -2608,16 +3593,18 @@ function registerAnalyticsTools(server, callApi) {
2608
3593
  "lexq_dataset_upload",
2609
3594
  {
2610
3595
  title: "Upload Dataset",
2611
- description: `Upload inline CSV or JSON content as a simulation dataset.
2612
- The content is uploaded to S3 and a path is returned.
2613
- Use this path in simulation start with dataset type UPLOADED.
2614
-
2615
- CSV example:
2616
- user_id,payment_amount
2617
- user_001,150000
2618
- user_002,50000
2619
-
2620
- JSON example: [{"user_id":"user_001","payment_amount":150000}, {"user_id":"user_002","payment_amount":50000}]`,
3596
+ description: dedent14`
3597
+ Upload inline CSV or JSON content as a simulation dataset.
3598
+ The content is uploaded to S3 and a path is returned.
3599
+ Use this path in simulation start with dataset type UPLOADED.
3600
+
3601
+ CSV example:
3602
+ user_id,payment_amount
3603
+ user_001,150000
3604
+ user_002,50000
3605
+
3606
+ JSON example: [{"user_id":"user_001","payment_amount":150000}, {"user_id":"user_002","payment_amount":50000}]
3607
+ `,
2621
3608
  inputSchema: {
2622
3609
  content: z6.string().describe("CSV or JSON content as string"),
2623
3610
  filename: z6.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
@@ -2825,14 +3812,14 @@ function registerLogTools(server, callApi) {
2825
3812
  "lexq_logs_action",
2826
3813
  {
2827
3814
  title: "Process Failure Log",
2828
- description: "Process a single failure log: RETRY, RESOLVE, or IGNORE.",
3815
+ description: "Process a single failure log: RETRY (re-execute with original payload), RESOLVE (mark as manually fixed), or IGNORE (skip intentionally).",
2829
3816
  inputSchema: {
2830
3817
  logId: z9.string().uuid().describe("Failure log ID"),
2831
3818
  action: z9.enum(FailureAction).describe("Action to take")
2832
3819
  }
2833
3820
  },
2834
3821
  async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
2835
- body: { action }
3822
+ params: { action }
2836
3823
  })
2837
3824
  );
2838
3825
  server.registerTool(
@@ -2851,6 +3838,76 @@ function registerLogTools(server, callApi) {
2851
3838
  );
2852
3839
  }
2853
3840
 
3841
+ // src/mcp/tools/webhook-subscriptions.ts
3842
+ import { z as z10 } from "zod";
3843
+ function registerWebhookSubscriptionTools(server, callApi) {
3844
+ server.registerTool(
3845
+ "lexq_webhook_subscriptions_list",
3846
+ {
3847
+ title: "List Webhook Subscriptions",
3848
+ description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
3849
+ inputSchema: {
3850
+ page: z10.number().int().min(0).default(0).describe("Page number"),
3851
+ size: z10.number().int().min(1).max(100).default(20).describe("Page size")
3852
+ }
3853
+ },
3854
+ async ({ page, size }) => {
3855
+ const params = paginationParams(page, size);
3856
+ return callApi("GET", "webhook-subscriptions", { params });
3857
+ }
3858
+ );
3859
+ server.registerTool(
3860
+ "lexq_webhook_subscriptions_get",
3861
+ {
3862
+ title: "Get Webhook Subscription",
3863
+ description: "Get webhook subscription detail by ID.",
3864
+ inputSchema: {
3865
+ id: z10.string().uuid().describe("Webhook subscription ID")
3866
+ }
3867
+ },
3868
+ async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
3869
+ );
3870
+ server.registerTool(
3871
+ "lexq_webhook_subscriptions_save",
3872
+ {
3873
+ title: "Save Webhook Subscription",
3874
+ description: 'Create or update a webhook subscription. Omit id to create, provide id to update. Events: VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED. Formats: GENERIC (full JSON), SLACK ({"text": "..."}).',
3875
+ inputSchema: {
3876
+ id: z10.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
3877
+ name: z10.string().min(1).describe("Subscription name (unique per tenant)"),
3878
+ webhookUrl: z10.string().url().describe("Webhook endpoint URL"),
3879
+ subscribedEvents: z10.array(z10.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
3880
+ payloadFormat: z10.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
3881
+ secret: z10.string().optional().describe("HMAC-SHA256 signing secret"),
3882
+ isActive: z10.boolean().optional().default(true).describe("Whether the subscription is active")
3883
+ }
3884
+ },
3885
+ async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
3886
+ );
3887
+ server.registerTool(
3888
+ "lexq_webhook_subscriptions_delete",
3889
+ {
3890
+ title: "Delete Webhook Subscription",
3891
+ description: "Delete a webhook subscription by ID.",
3892
+ inputSchema: {
3893
+ id: z10.string().uuid().describe("Webhook subscription ID")
3894
+ }
3895
+ },
3896
+ async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
3897
+ );
3898
+ server.registerTool(
3899
+ "lexq_webhook_subscriptions_test",
3900
+ {
3901
+ title: "Test Webhook Subscription",
3902
+ description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
3903
+ inputSchema: {
3904
+ id: z10.string().uuid().describe("Webhook subscription ID")
3905
+ }
3906
+ },
3907
+ async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
3908
+ );
3909
+ }
3910
+
2854
3911
  // src/mcp/register.ts
2855
3912
  function registerAllTools(server, callApi) {
2856
3913
  registerStatusTools(server, callApi);
@@ -2863,6 +3920,7 @@ function registerAllTools(server, callApi) {
2863
3920
  registerHistoryTools(server, callApi);
2864
3921
  registerIntegrationTools(server, callApi);
2865
3922
  registerLogTools(server, callApi);
3923
+ registerWebhookSubscriptionTools(server, callApi);
2866
3924
  }
2867
3925
 
2868
3926
  // src/mcp/server.ts
@@ -2888,7 +3946,27 @@ async function startMcpServer() {
2888
3946
 
2889
3947
  // src/commands/serve.ts
2890
3948
  function registerServeCommand(program) {
2891
- program.command("serve").description("Start LexQ as a server for AI agent integrations").option("--mcp", "Start as MCP (Model Context Protocol) server over stdio").action(async (opts) => {
3949
+ program.command("serve").description("Start LexQ as a server for AI agent integrations").option("--mcp", "Start as MCP (Model Context Protocol) server over stdio").addHelpText(
3950
+ "after",
3951
+ dedent15`
3952
+
3953
+ Example:
3954
+ $ lexq serve --mcp
3955
+
3956
+ Starts a stdio MCP server that exposes 60 tools for policy management.
3957
+ Used by Claude Desktop, Claude.ai, Cursor, and other MCP-compatible clients.
3958
+
3959
+ Claude Desktop config (~/.claude/claude_desktop_config.json):
3960
+ {
3961
+ "mcpServers": {
3962
+ "lexq": {
3963
+ "command": "npx",
3964
+ "args": ["-y", "@lexq/cli", "serve", "--mcp"]
3965
+ }
3966
+ }
3967
+ }
3968
+ `
3969
+ ).action(async (opts) => {
2892
3970
  if (!opts.mcp) {
2893
3971
  console.error(
2894
3972
  "Error: --mcp flag is required.\nUsage: lexq serve --mcp\n\nStarts a stdio MCP server for Claude Desktop, Claude.ai, Gemini, etc."
@@ -2923,6 +4001,7 @@ function createCli() {
2923
4001
  registerHistoryCommands(program);
2924
4002
  registerIntegrationCommands(program);
2925
4003
  registerLogCommands(program);
4004
+ registerWebhookSubscriptionCommands(program);
2926
4005
  registerServeCommand(program);
2927
4006
  return program;
2928
4007
  }