@lexq/cli 0.1.17 → 0.1.19

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,7 +212,15 @@ 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 {
192
225
  const config = loadConfig();
193
226
  if (!config.apiKey) {
@@ -213,8 +246,19 @@ function registerAuthCommands(program) {
213
246
 
214
247
  // src/commands/status.ts
215
248
  import "commander";
249
+ import dedent2 from "dedent";
216
250
  function registerStatusCommand(program) {
217
- program.command("status").description("Check API connectivity and authentication").action(async () => {
251
+ program.command("status").description("Check API connectivity and authentication").addHelpText(
252
+ "after",
253
+ dedent2`
254
+
255
+ Example:
256
+ $ lexq status
257
+ { "status": "ok", "latencyMs": 142, "tenantId": "abc-123", "role": "ADMIN" }
258
+
259
+ Use this to verify your API key is valid and the LexQ API is reachable.
260
+ `
261
+ ).action(async () => {
218
262
  const globalOpts = program.opts();
219
263
  const startTime = Date.now();
220
264
  try {
@@ -237,8 +281,26 @@ function registerStatusCommand(program) {
237
281
 
238
282
  // src/commands/groups.ts
239
283
  import "commander";
284
+ import dedent3 from "dedent";
240
285
  function registerGroupCommands(program) {
241
- const groups = program.command("groups").description("Manage policy groups");
286
+ const groups = program.command("groups").description("Manage policy groups").addHelpText(
287
+ "after",
288
+ dedent3`
289
+
290
+ A policy group is the top-level container for rule versions.
291
+ It controls deployment lifecycle, conflict resolution, and A/B testing.
292
+
293
+ Commands:
294
+ list List all policy groups
295
+ get Get group detail by ID
296
+ create Create a new group
297
+ update Update group settings
298
+ delete Archive a group
299
+ ab-test Manage A/B tests (start, stop, adjust)
300
+
301
+ Statuses: ACTIVE, DISABLED (emergency stop), ARCHIVED (soft delete)
302
+ `
303
+ );
242
304
  groups.command("list").description("List all policy groups").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
243
305
  try {
244
306
  const globalOpts = program.opts();
@@ -288,7 +350,28 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
288
350
  process.exit(1);
289
351
  }
290
352
  });
291
- groups.command("create").description("Create a new policy group").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
353
+ groups.command("create").description("Create a new policy group").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
354
+ "after",
355
+ dedent3`
356
+
357
+ Example:
358
+ $ lexq groups create --json '{
359
+ "name": "Payment Policy",
360
+ "description": "Rules for payment processing",
361
+ "priority": 0
362
+ }'
363
+
364
+ Fields:
365
+ name string Group name (required, unique per tenant)
366
+ description string Description (optional, max 255 chars)
367
+ priority number Execution priority — lower runs first (required, min 0)
368
+ activationGroup string Cross-group conflict resolution key (optional)
369
+ activationMode string NONE | EXCLUSIVE | MAX_N [default: NONE]
370
+ activationStrategy string FIRST_MATCH | HIGHEST_PRIORITY | MAX_BENEFIT [default: FIRST_MATCH]
371
+ executionLimit number Max rules to fire in MAX_N mode (optional)
372
+ status string ACTIVE | DISABLED [default: ACTIVE]
373
+ `
374
+ ).action(async (opts) => {
292
375
  try {
293
376
  const globalOpts = program.opts();
294
377
  const body = JSON.parse(opts.json);
@@ -305,7 +388,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
305
388
  process.exit(1);
306
389
  }
307
390
  });
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) => {
391
+ groups.command("update").description("Update a policy group").requiredOption("--id <groupId>", "Policy group ID").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
392
+ "after",
393
+ dedent3`
394
+
395
+ Example:
396
+ $ lexq groups update --id <groupId> --json '{"description": "Updated", "priority": 1}'
397
+
398
+ All fields are optional — only provided fields are updated.
399
+ `
400
+ ).action(async (opts) => {
309
401
  try {
310
402
  const globalOpts = program.opts();
311
403
  const body = JSON.parse(opts.json);
@@ -322,7 +414,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
322
414
  process.exit(1);
323
415
  }
324
416
  });
325
- groups.command("delete").description("Delete a policy group").requiredOption("--id <groupId>", "Policy group ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
417
+ groups.command("delete").description("Delete a policy group").requiredOption("--id <groupId>", "Policy group ID").option("--force", "Skip confirmation prompt").addHelpText(
418
+ "after",
419
+ dedent3`
420
+
421
+ This archives the group (soft delete). Use --force to skip the confirmation prompt.
422
+ `
423
+ ).action(async (opts) => {
326
424
  try {
327
425
  const globalOpts = program.opts();
328
426
  if (!opts.force) {
@@ -347,8 +445,31 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
347
445
  process.exit(1);
348
446
  }
349
447
  });
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) => {
448
+ const abTest = groups.command("ab-test").description("A/B test management").addHelpText(
449
+ "after",
450
+ dedent3`
451
+
452
+ Split traffic between the current live version and a challenger version.
453
+
454
+ Commands:
455
+ start Start an A/B test with a challenger version
456
+ stop Stop the test and revert to 100% live version
457
+ adjust Change the traffic percentage
458
+
459
+ The traffic rate (1-99) determines what percentage goes to the challenger.
460
+ The remaining traffic continues to the current live version.
461
+ `
462
+ );
463
+ 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(
464
+ "after",
465
+ dedent3`
466
+
467
+ Example:
468
+ $ lexq groups ab-test start --group-id <gid> --version-id <vid> --traffic-rate 20
469
+
470
+ Routes 20% of traffic to the challenger version, 80% to the current live version.
471
+ `
472
+ ).action(async (opts) => {
352
473
  try {
353
474
  const globalOpts = program.opts();
354
475
  const body = {
@@ -397,7 +518,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
397
518
  process.exit(1);
398
519
  }
399
520
  });
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) => {
521
+ 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(
522
+ "after",
523
+ dedent3`
524
+
525
+ Example:
526
+ $ lexq groups ab-test adjust --group-id <gid> --traffic-rate 50
527
+ `
528
+ ).action(async (opts) => {
401
529
  try {
402
530
  const globalOpts = program.opts();
403
531
  const body = {
@@ -424,8 +552,27 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
424
552
 
425
553
  // src/commands/versions.ts
426
554
  import "commander";
555
+ import dedent4 from "dedent";
427
556
  function registerVersionCommands(program) {
428
- const versions = program.command("versions").description("Manage policy versions");
557
+ const versions = program.command("versions").description("Manage policy versions").addHelpText(
558
+ "after",
559
+ dedent4`
560
+
561
+ A version is an immutable snapshot of rules within a policy group.
562
+
563
+ Lifecycle: DRAFT → ACTIVE (publish) → ARCHIVED (superseded) | EXPIRED (past effectiveTo)
564
+
565
+ Commands:
566
+ list List versions for a group
567
+ get Get version detail
568
+ create Create a new DRAFT version
569
+ update Update DRAFT version metadata
570
+ delete Delete a DRAFT version
571
+ clone Duplicate a version (creates a new DRAFT with same rules)
572
+
573
+ Only DRAFT versions can be modified. Published versions are locked.
574
+ `
575
+ );
429
576
  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
577
  try {
431
578
  const globalOpts = program.opts();
@@ -483,7 +630,25 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
483
630
  process.exit(1);
484
631
  }
485
632
  });
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) => {
633
+ 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(
634
+ "after",
635
+ dedent4`
636
+
637
+ Examples:
638
+ $ lexq versions create --group-id <gid> --commit-message "Initial version"
639
+
640
+ $ lexq versions create --group-id <gid> --json '{
641
+ "commitMessage": "Seasonal promo",
642
+ "effectiveFrom": "2026-06-01T00:00:00Z",
643
+ "effectiveTo": "2026-08-31T23:59:59Z"
644
+ }'
645
+
646
+ Fields:
647
+ commitMessage string Version description (optional, max 255 chars)
648
+ effectiveFrom datetime Start of effective period (optional, ISO-8601)
649
+ effectiveTo datetime End of effective period (optional, auto-expires)
650
+ `
651
+ ).action(async (opts) => {
487
652
  try {
488
653
  const globalOpts = program.opts();
489
654
  const body = opts.json ? JSON.parse(opts.json) : buildCreateBody(opts);
@@ -504,7 +669,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
504
669
  process.exit(1);
505
670
  }
506
671
  });
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) => {
672
+ 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(
673
+ "after",
674
+ dedent4`
675
+
676
+ Only DRAFT versions can be updated. Published (ACTIVE) versions are immutable.
677
+
678
+ Example:
679
+ $ lexq versions update --group-id <gid> --id <vid> --commit-message "Updated rules"
680
+ `
681
+ ).action(async (opts) => {
508
682
  try {
509
683
  const globalOpts = program.opts();
510
684
  const body = opts.json ? JSON.parse(opts.json) : buildUpdateBody(opts);
@@ -525,7 +699,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
525
699
  process.exit(1);
526
700
  }
527
701
  });
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) => {
702
+ 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(
703
+ "after",
704
+ dedent4`
705
+
706
+ Only DRAFT versions can be deleted. Use --force to skip confirmation.
707
+ `
708
+ ).action(async (opts) => {
529
709
  try {
530
710
  const globalOpts = program.opts();
531
711
  if (!opts.force) {
@@ -550,7 +730,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
550
730
  process.exit(1);
551
731
  }
552
732
  });
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) => {
733
+ 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(
734
+ "after",
735
+ dedent4`
736
+
737
+ Creates a new DRAFT version with all rules copied from the source.
738
+ Use this to iterate on a published version without modifying it.
739
+
740
+ Example:
741
+ $ lexq versions clone --group-id <gid> --id <source-vid>
742
+ `
743
+ ).action(async (opts) => {
554
744
  try {
555
745
  const globalOpts = program.opts();
556
746
  const data = await apiRequest(
@@ -587,8 +777,27 @@ function buildUpdateBody(opts) {
587
777
 
588
778
  // src/commands/rules.ts
589
779
  import "commander";
780
+ import dedent5 from "dedent";
590
781
  function registerRuleCommands(program) {
591
- const rules = program.command("rules").description("Manage policy rules");
782
+ const rules = program.command("rules").description("Manage policy rules").addHelpText(
783
+ "after",
784
+ dedent5`
785
+
786
+ Rules define condition → action pairs within a version.
787
+ They are evaluated in priority order (lower number = higher priority).
788
+
789
+ Commands:
790
+ list List rules in a version
791
+ get Get rule detail
792
+ create Add a new rule to a DRAFT version
793
+ update Modify a rule in a DRAFT version
794
+ delete Remove a rule from a DRAFT version
795
+ reorder Change rule priorities (drag & drop equivalent)
796
+ toggle Enable or disable a rule
797
+
798
+ Only DRAFT versions allow rule modifications.
799
+ `
800
+ );
592
801
  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
802
  try {
594
803
  const globalOpts = program.opts();
@@ -646,7 +855,41 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
646
855
  process.exit(1);
647
856
  }
648
857
  });
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) => {
858
+ 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(
859
+ "after",
860
+ dedent5`
861
+
862
+ Example:
863
+ $ lexq rules create --group-id <gid> --version-id <vid> --json '{
864
+ "name": "VIP 20% Discount",
865
+ "priority": 0,
866
+ "condition": {
867
+ "type": "SINGLE",
868
+ "field": "customer_tier",
869
+ "operator": "EQUALS",
870
+ "value": "VIP",
871
+ "valueType": "STRING"
872
+ },
873
+ "actions": [
874
+ { "type": "DISCOUNT", "parameters": { "method": "PERCENTAGE", "rate": 20, "refVar": "payment_amount" } }
875
+ ]
876
+ }'
877
+
878
+ Condition Operators:
879
+ EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL,
880
+ LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
881
+
882
+ Action Types:
883
+ DISCOUNT, POINT, COUPON_ISSUE, BLOCK, NOTIFICATION, WEBHOOK, SET_FACT, ADD_TAG
884
+
885
+ Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
886
+
887
+ Mutex (optional — rule-level conflict resolution):
888
+ mutexGroup string Logical grouping key (e.g., "best-discount")
889
+ mutexMode string NONE | EXCLUSIVE [default: NONE]
890
+ mutexStrategy string FIRST_MATCH | HIGHEST_PRIORITY | MAX_BENEFIT
891
+ `
892
+ ).action(async (opts) => {
650
893
  try {
651
894
  const globalOpts = program.opts();
652
895
  const body = JSON.parse(opts.json);
@@ -667,7 +910,21 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
667
910
  process.exit(1);
668
911
  }
669
912
  });
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) => {
913
+ 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(
914
+ "after",
915
+ dedent5`
916
+
917
+ Same fields as create. Only DRAFT versions can be modified.
918
+
919
+ Example:
920
+ $ lexq rules update --group-id <gid> --version-id <vid> --id <rid> --json '{
921
+ "name": "VIP 25% Discount",
922
+ "actions": [
923
+ { "type": "DISCOUNT", "parameters": { "method": "PERCENTAGE", "rate": 25, "refVar": "payment_amount" } }
924
+ ]
925
+ }'
926
+ `
927
+ ).action(async (opts) => {
671
928
  try {
672
929
  const globalOpts = program.opts();
673
930
  const body = JSON.parse(opts.json);
@@ -717,7 +974,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
717
974
  process.exit(1);
718
975
  }
719
976
  });
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) => {
977
+ 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(
978
+ "after",
979
+ dedent5`
980
+
981
+ Assigns priority 0, 1, 2, ... to rules in the order given.
982
+
983
+ Example:
984
+ $ lexq rules reorder --group-id <gid> --version-id <vid> --rule-ids "id3,id1,id2"
985
+
986
+ Result: id3 → priority 0, id1 → priority 1, id2 → priority 2
987
+ `
988
+ ).action(async (opts) => {
721
989
  try {
722
990
  const globalOpts = program.opts();
723
991
  const ruleIds = opts.ruleIds.split(",").map((id) => id.trim());
@@ -744,7 +1012,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
744
1012
  process.exit(1);
745
1013
  }
746
1014
  });
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) => {
1015
+ 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(
1016
+ "after",
1017
+ dedent5`
1018
+
1019
+ Disabled rules are skipped during execution without deleting them.
1020
+
1021
+ Example:
1022
+ $ lexq rules toggle --group-id <gid> --version-id <vid> --id <rid> --enabled false
1023
+ `
1024
+ ).action(async (opts) => {
748
1025
  try {
749
1026
  const globalOpts = program.opts();
750
1027
  const isEnabled = opts.enabled === "true";
@@ -769,16 +1046,30 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
769
1046
 
770
1047
  // src/commands/facts.ts
771
1048
  import "commander";
1049
+ import dedent6 from "dedent";
772
1050
  function registerFactCommands(program) {
773
- const facts = program.command("facts").description("Manage fact definitions (schema)");
1051
+ const facts = program.command("facts").description("Manage fact definitions (schema)").addHelpText(
1052
+ "after",
1053
+ dedent6`
1054
+
1055
+ Facts are input variables passed during policy execution.
1056
+ Define them here so rules can reference them in conditions and actions.
1057
+
1058
+ Commands:
1059
+ list List all fact definitions
1060
+ create Register a new fact
1061
+ update Update fact metadata
1062
+ delete Remove a fact definition
1063
+ action-metadata Show action runtime fact metadata
1064
+
1065
+ System facts (payment_amount, user_id, etc.) are auto-created and immutable.
1066
+ `
1067
+ );
774
1068
  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
1069
  try {
776
1070
  const globalOpts = program.opts();
777
1071
  const format = globalOpts.format ?? "json";
778
- const params = {
779
- page: opts.page,
780
- size: opts.size
781
- };
1072
+ const params = { page: opts.page, size: opts.size };
782
1073
  if (opts.keyword) params.keyword = opts.keyword;
783
1074
  const data = await apiRequest("GET", "schema/facts", {
784
1075
  apiKey: globalOpts.apiKey,
@@ -810,7 +1101,26 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
810
1101
  process.exit(1);
811
1102
  }
812
1103
  });
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) => {
1104
+ 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(
1105
+ "after",
1106
+ dedent6`
1107
+
1108
+ Examples:
1109
+ $ lexq facts create --key customer_tier --name "Customer Tier" --type STRING
1110
+ $ lexq facts create --key order_total --name "Order Total" --type NUMBER --required
1111
+
1112
+ $ lexq facts create --json '{
1113
+ "key": "user_region",
1114
+ "name": "User Region",
1115
+ "type": "STRING",
1116
+ "description": "ISO country code",
1117
+ "isRequired": false
1118
+ }'
1119
+
1120
+ Value Types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
1121
+ Key Format: lowercase letters, numbers, underscores only (e.g., payment_amount)
1122
+ `
1123
+ ).action(async (opts) => {
814
1124
  try {
815
1125
  const globalOpts = program.opts();
816
1126
  const body = opts.json ? JSON.parse(opts.json) : buildCreateBody2(opts);
@@ -827,7 +1137,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
827
1137
  process.exit(1);
828
1138
  }
829
1139
  });
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) => {
1140
+ 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(
1141
+ "after",
1142
+ dedent6`
1143
+
1144
+ System facts cannot be modified. Only display name, description, and required flag can be changed.
1145
+
1146
+ Example:
1147
+ $ lexq facts update --id <factId> --name "Updated Name" --required
1148
+ `
1149
+ ).action(async (opts) => {
831
1150
  try {
832
1151
  const globalOpts = program.opts();
833
1152
  const body = opts.json ? JSON.parse(opts.json) : buildUpdateBody2(opts);
@@ -844,7 +1163,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
844
1163
  process.exit(1);
845
1164
  }
846
1165
  });
847
- facts.command("delete").description("Delete a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
1166
+ facts.command("delete").description("Delete a fact definition").requiredOption("--id <factId>", "Fact definition ID").option("--force", "Skip confirmation prompt").addHelpText(
1167
+ "after",
1168
+ dedent6`
1169
+
1170
+ System facts cannot be deleted. Use --force to skip the confirmation prompt.
1171
+ `
1172
+ ).action(async (opts) => {
848
1173
  try {
849
1174
  const globalOpts = program.opts();
850
1175
  if (!opts.force) {
@@ -869,7 +1194,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
869
1194
  process.exit(1);
870
1195
  }
871
1196
  });
872
- facts.command("action-metadata").description("Get action runtime fact metadata").action(async () => {
1197
+ facts.command("action-metadata").description("Get action runtime fact metadata").addHelpText(
1198
+ "after",
1199
+ dedent6`
1200
+
1201
+ Shows which facts are automatically created by each action type at runtime.
1202
+ Useful for understanding what output variables are available after rule execution.
1203
+ `
1204
+ ).action(async () => {
873
1205
  try {
874
1206
  const globalOpts = program.opts();
875
1207
  const data = await apiRequest("GET", "schema/action-metadata", {
@@ -886,9 +1218,8 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
886
1218
  });
887
1219
  }
888
1220
  function buildCreateBody2(opts) {
889
- if (!opts.key || !opts.name || !opts.type) {
1221
+ if (!opts.key || !opts.name || !opts.type)
890
1222
  throw new Error("--key, --name, and --type are required (or use --json).");
891
- }
892
1223
  const body = {
893
1224
  key: opts.key,
894
1225
  name: opts.name,
@@ -908,9 +1239,39 @@ function buildUpdateBody2(opts) {
908
1239
 
909
1240
  // src/commands/deploy.ts
910
1241
  import "commander";
1242
+ import dedent7 from "dedent";
911
1243
  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) => {
1244
+ const deploy = program.command("deploy").description("Deployment lifecycle and history").addHelpText(
1245
+ "after",
1246
+ dedent7`
1247
+
1248
+ Lifecycle: Publish (DRAFT→ACTIVE) → Deploy (ACTIVE→LIVE) → Rollback / Undeploy
1249
+
1250
+ Commands:
1251
+ publish Lock a DRAFT version (DRAFT → ACTIVE)
1252
+ live Push an ACTIVE version to production traffic
1253
+ rollback Revert to the previous deployed version
1254
+ undeploy Remove the live version (stops all traffic)
1255
+ history List deployment history with filters
1256
+ detail Get deployment detail with integrity check
1257
+ overview Show all groups' deployment status at a glance
1258
+ deployable List ACTIVE versions available for deployment
1259
+ diff Compare rule snapshots between two versions
1260
+
1261
+ Always dry-run before publishing. Cannot deploy a DRAFT — publish first.
1262
+ `
1263
+ );
1264
+ 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(
1265
+ "after",
1266
+ dedent7`
1267
+
1268
+ Locks the version permanently. Rules cannot be modified after publishing.
1269
+ A snapshot hash is generated for integrity verification.
1270
+
1271
+ Example:
1272
+ $ lexq deploy publish --group-id <gid> --version-id <vid> --memo "Validated via dry-run"
1273
+ `
1274
+ ).action(async (opts) => {
914
1275
  try {
915
1276
  const globalOpts = program.opts();
916
1277
  await apiRequest(
@@ -930,7 +1291,16 @@ function registerDeployCommands(program) {
930
1291
  process.exit(1);
931
1292
  }
932
1293
  });
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) => {
1294
+ 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(
1295
+ "after",
1296
+ dedent7`
1297
+
1298
+ Takes effect immediately. The version starts receiving production traffic.
1299
+
1300
+ Example:
1301
+ $ lexq deploy live --group-id <gid> --version-id <vid> --memo "Go live — v3"
1302
+ `
1303
+ ).action(async (opts) => {
934
1304
  try {
935
1305
  const globalOpts = program.opts();
936
1306
  await apiRequest("POST", `policy-groups/${opts.groupId}/deploy`, {
@@ -946,7 +1316,17 @@ function registerDeployCommands(program) {
946
1316
  process.exit(1);
947
1317
  }
948
1318
  });
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) => {
1319
+ 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(
1320
+ "after",
1321
+ dedent7`
1322
+
1323
+ Reverts to the version that was live before the current one.
1324
+ Only available if the previous version is still ACTIVE.
1325
+
1326
+ Example:
1327
+ $ lexq deploy rollback --group-id <gid> --memo "High error rate" --force
1328
+ `
1329
+ ).action(async (opts) => {
950
1330
  try {
951
1331
  const globalOpts = program.opts();
952
1332
  if (!opts.force) {
@@ -972,7 +1352,16 @@ function registerDeployCommands(program) {
972
1352
  process.exit(1);
973
1353
  }
974
1354
  });
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) => {
1355
+ 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(
1356
+ "after",
1357
+ dedent7`
1358
+
1359
+ Stops all traffic processing for this group until a new version is deployed.
1360
+
1361
+ Example:
1362
+ $ lexq deploy undeploy --group-id <gid> --memo "Maintenance window" --force
1363
+ `
1364
+ ).action(async (opts) => {
976
1365
  try {
977
1366
  const globalOpts = program.opts();
978
1367
  if (!opts.force) {
@@ -1001,14 +1390,18 @@ function registerDeployCommands(program) {
1001
1390
  deploy.command("history").description("List deployment history").option("--group-id <groupId>", "Filter by policy group").option(
1002
1391
  "--types <types>",
1003
1392
  "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) => {
1393
+ ).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(
1394
+ "after",
1395
+ dedent7`
1396
+
1397
+ Example:
1398
+ $ lexq deploy history --group-id <gid> --types DEPLOY,ROLLBACK --format table
1399
+ `
1400
+ ).action(async (opts) => {
1005
1401
  try {
1006
1402
  const globalOpts = program.opts();
1007
1403
  const format = globalOpts.format ?? "json";
1008
- const params = {
1009
- page: opts.page,
1010
- size: opts.size
1011
- };
1404
+ const params = { page: opts.page, size: opts.size };
1012
1405
  if (opts.groupId) params.groupId = opts.groupId;
1013
1406
  if (opts.types) params.types = opts.types;
1014
1407
  if (opts.startDate) params.startDate = opts.startDate;
@@ -1043,7 +1436,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1043
1436
  process.exit(1);
1044
1437
  }
1045
1438
  });
1046
- deploy.command("detail").description("Get deployment detail").requiredOption("--id <deploymentId>", "Deployment ID").action(async (opts) => {
1439
+ deploy.command("detail").description("Get deployment detail").requiredOption("--id <deploymentId>", "Deployment ID").addHelpText(
1440
+ "after",
1441
+ dedent7`
1442
+
1443
+ Includes snapshot hash and integrity check (hashValid field).
1444
+ `
1445
+ ).action(async (opts) => {
1047
1446
  try {
1048
1447
  const globalOpts = program.opts();
1049
1448
  const data = await apiRequest("GET", `deployments/${opts.id}`, {
@@ -1058,7 +1457,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1058
1457
  process.exit(1);
1059
1458
  }
1060
1459
  });
1061
- deploy.command("overview").description("Show deployment status overview for all groups").action(async () => {
1460
+ deploy.command("overview").description("Show deployment status overview for all groups").addHelpText(
1461
+ "after",
1462
+ dedent7`
1463
+
1464
+ Shows which version is live for each group, who deployed it, and when.
1465
+
1466
+ Example:
1467
+ $ lexq deploy overview --format table
1468
+ `
1469
+ ).action(async () => {
1062
1470
  try {
1063
1471
  const globalOpts = program.opts();
1064
1472
  const format = globalOpts.format ?? "json";
@@ -1087,7 +1495,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1087
1495
  process.exit(1);
1088
1496
  }
1089
1497
  });
1090
- deploy.command("deployable").description("List deployable (ACTIVE) versions for a group").requiredOption("--group-id <groupId>", "Policy group ID").action(async (opts) => {
1498
+ deploy.command("deployable").description("List deployable (ACTIVE) versions for a group").requiredOption("--group-id <groupId>", "Policy group ID").addHelpText(
1499
+ "after",
1500
+ dedent7`
1501
+
1502
+ Shows ACTIVE versions that can be deployed. Only published versions appear here.
1503
+ `
1504
+ ).action(async (opts) => {
1091
1505
  try {
1092
1506
  const globalOpts = program.opts();
1093
1507
  const data = await apiRequest(
@@ -1106,7 +1520,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1106
1520
  process.exit(1);
1107
1521
  }
1108
1522
  });
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) => {
1523
+ deploy.command("diff").description("Compare snapshot diff between two versions").requiredOption("--base <versionId>", "Base version ID").requiredOption("--target <versionId>", "Target version ID").addHelpText(
1524
+ "after",
1525
+ dedent7`
1526
+
1527
+ Shows added, removed, and modified rules between two versions.
1528
+
1529
+ Example:
1530
+ $ lexq deploy diff --base <v1-id> --target <v2-id>
1531
+ `
1532
+ ).action(async (opts) => {
1110
1533
  try {
1111
1534
  const globalOpts = program.opts();
1112
1535
  const data = await apiRequest("GET", "deployments/diff", {
@@ -1114,10 +1537,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1114
1537
  baseUrl: globalOpts.baseUrl,
1115
1538
  dryRun: globalOpts.dryRun,
1116
1539
  verbose: globalOpts.verbose,
1117
- params: {
1118
- baseVersionId: opts.base,
1119
- targetVersionId: opts.target
1120
- }
1540
+ params: { baseVersionId: opts.base, targetVersionId: opts.target }
1121
1541
  });
1122
1542
  printJson(data);
1123
1543
  } catch (error) {
@@ -1130,9 +1550,38 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1130
1550
  // src/commands/analytics.ts
1131
1551
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1132
1552
  import "commander";
1553
+ import dedent8 from "dedent";
1133
1554
  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) => {
1555
+ const analytics = program.command("analytics").description("Dry run, simulation, and requirements").addHelpText(
1556
+ "after",
1557
+ dedent8`
1558
+
1559
+ Test and validate rules before deploying to production.
1560
+
1561
+ Commands:
1562
+ dry-run Test a single input against a version
1563
+ dry-run-compare Compare results between two versions
1564
+ requirements Show required input facts for a version
1565
+ simulation Batch test against historical data (start, status, list, cancel, export)
1566
+ dataset Upload datasets and download templates
1567
+
1568
+ Workflow: facts check → dry-run → publish → simulation → deploy
1569
+ `
1570
+ );
1571
+ 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(
1572
+ "after",
1573
+ dedent8`
1574
+
1575
+ Examples:
1576
+ $ lexq analytics dry-run --version-id <vid> --debug --mock \\
1577
+ --json '{"facts": {"payment_amount": 150000, "customer_tier": "VIP"}}'
1578
+
1579
+ $ lexq analytics dry-run --version-id <vid> --file test-input.json
1580
+
1581
+ The request body must include a "facts" object. Use --debug for execution traces
1582
+ and --mock to skip external service calls (webhooks, coupons, etc.).
1583
+ `
1584
+ ).action(async (opts) => {
1136
1585
  try {
1137
1586
  const globalOpts = program.opts();
1138
1587
  const body = resolveBody(opts);
@@ -1158,7 +1607,20 @@ function registerAnalyticsCommands(program) {
1158
1607
  process.exit(1);
1159
1608
  }
1160
1609
  });
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) => {
1610
+ 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(
1611
+ "after",
1612
+ dedent8`
1613
+
1614
+ Example:
1615
+ $ lexq analytics dry-run-compare --json '{
1616
+ "versionIdA": "<version-a-id>",
1617
+ "versionIdB": "<version-b-id>",
1618
+ "facts": {"payment_amount": 100000, "customer_tier": "VIP"}
1619
+ }'
1620
+
1621
+ Shows side-by-side which rules matched and what actions fired for each version.
1622
+ `
1623
+ ).action(async (opts) => {
1162
1624
  try {
1163
1625
  const globalOpts = program.opts();
1164
1626
  const body = resolveBody(opts);
@@ -1180,7 +1642,16 @@ function registerAnalyticsCommands(program) {
1180
1642
  process.exit(1);
1181
1643
  }
1182
1644
  });
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) => {
1645
+ 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(
1646
+ "after",
1647
+ dedent8`
1648
+
1649
+ Shows all facts referenced in conditions and actions, along with an example request body.
1650
+
1651
+ Example:
1652
+ $ lexq analytics requirements --group-id <gid> --version-id <vid> --format table
1653
+ `
1654
+ ).action(async (opts) => {
1184
1655
  try {
1185
1656
  const globalOpts = program.opts();
1186
1657
  const format = globalOpts.format ?? "json";
@@ -1215,8 +1686,44 @@ function registerAnalyticsCommands(program) {
1215
1686
  process.exit(1);
1216
1687
  }
1217
1688
  });
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) => {
1689
+ const sim = analytics.command("simulation").description("Manage batch simulations").addHelpText(
1690
+ "after",
1691
+ dedent8`
1692
+
1693
+ Batch-test a version against historical data or uploaded datasets.
1694
+
1695
+ Commands:
1696
+ start Start a new simulation
1697
+ status Check progress and results
1698
+ list List simulation history
1699
+ cancel Cancel a running simulation
1700
+ export Export results as CSV or JSON
1701
+
1702
+ Simulations always mock external calls. Use --format table for summary view.
1703
+ `
1704
+ );
1705
+ 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(
1706
+ "after",
1707
+ dedent8`
1708
+
1709
+ Example:
1710
+ $ lexq analytics simulation start --json '{
1711
+ "policyVersionId": "<vid>",
1712
+ "dataset": {
1713
+ "type": "EXECUTION_LOG",
1714
+ "source": "RECENT",
1715
+ "maxRecords": 1000
1716
+ },
1717
+ "options": {
1718
+ "baselinePolicyVersionId": "<baseline-vid>",
1719
+ "includeRuleStats": true
1720
+ }
1721
+ }'
1722
+
1723
+ Dataset types: EXECUTION_LOG, MANUAL
1724
+ Dataset sources: RECENT, DATE_RANGE, MANUAL
1725
+ `
1726
+ ).action(async (opts) => {
1220
1727
  try {
1221
1728
  const globalOpts = program.opts();
1222
1729
  const body = resolveBody(opts);
@@ -1234,7 +1741,16 @@ function registerAnalyticsCommands(program) {
1234
1741
  process.exit(1);
1235
1742
  }
1236
1743
  });
1237
- sim.command("status").description("Get simulation status and results").requiredOption("--id <simulationId>", "Simulation ID").action(async (opts) => {
1744
+ sim.command("status").description("Get simulation status and results").requiredOption("--id <simulationId>", "Simulation ID").addHelpText(
1745
+ "after",
1746
+ dedent8`
1747
+
1748
+ Shows progress, match rate, metric comparison (if baseline set), and per-rule stats.
1749
+
1750
+ Example:
1751
+ $ lexq analytics simulation status --id <simId> --format table
1752
+ `
1753
+ ).action(async (opts) => {
1238
1754
  try {
1239
1755
  const globalOpts = program.opts();
1240
1756
  const format = globalOpts.format ?? "json";
@@ -1314,10 +1830,7 @@ function registerAnalyticsCommands(program) {
1314
1830
  try {
1315
1831
  const globalOpts = program.opts();
1316
1832
  const format = globalOpts.format ?? "json";
1317
- const params = {
1318
- page: opts.page,
1319
- size: opts.size
1320
- };
1833
+ const params = { page: opts.page, size: opts.size };
1321
1834
  if (opts.status) params.status = opts.status;
1322
1835
  if (opts.from) params.from = opts.from;
1323
1836
  if (opts.to) params.to = opts.to;
@@ -1356,7 +1869,13 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1356
1869
  process.exit(1);
1357
1870
  }
1358
1871
  });
1359
- sim.command("cancel").description("Cancel a running simulation").requiredOption("--id <simulationId>", "Simulation ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
1872
+ sim.command("cancel").description("Cancel a running simulation").requiredOption("--id <simulationId>", "Simulation ID").option("--force", "Skip confirmation prompt").addHelpText(
1873
+ "after",
1874
+ dedent8`
1875
+
1876
+ Only PENDING or RUNNING simulations can be cancelled.
1877
+ `
1878
+ ).action(async (opts) => {
1360
1879
  try {
1361
1880
  const globalOpts = program.opts();
1362
1881
  if (!opts.force) {
@@ -1381,7 +1900,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1381
1900
  process.exit(1);
1382
1901
  }
1383
1902
  });
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) => {
1903
+ 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(
1904
+ "after",
1905
+ dedent8`
1906
+
1907
+ Only COMPLETED simulations can be exported.
1908
+
1909
+ Examples:
1910
+ $ lexq analytics simulation export --id <simId> --format csv --output results.csv
1911
+ $ lexq analytics simulation export --id <simId> --format json
1912
+ `
1913
+ ).action(async (opts) => {
1385
1914
  try {
1386
1915
  const globalOpts = program.opts();
1387
1916
  const exportFormat = opts.format === "csv" ? "csv" : "json";
@@ -1408,8 +1937,26 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1408
1937
  process.exit(1);
1409
1938
  }
1410
1939
  });
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) => {
1940
+ const dataset = analytics.command("dataset").description("Upload datasets and download templates").addHelpText(
1941
+ "after",
1942
+ dedent8`
1943
+
1944
+ Commands:
1945
+ upload Upload a CSV or JSON file as a simulation dataset
1946
+ template Download a dataset template based on version requirements
1947
+ `
1948
+ );
1949
+ dataset.command("upload").description("Upload a CSV or JSON file as a simulation dataset").requiredOption("--file <path>", "Path to CSV or JSON file").addHelpText(
1950
+ "after",
1951
+ dedent8`
1952
+
1953
+ Supported formats: CSV (with header row), JSON (array of objects).
1954
+ Use "dataset template" to generate a correctly formatted template.
1955
+
1956
+ Example:
1957
+ $ lexq analytics dataset upload --file transactions.csv
1958
+ `
1959
+ ).action(async (opts) => {
1413
1960
  try {
1414
1961
  const globalOpts = program.opts();
1415
1962
  const config = loadConfig();
@@ -1455,7 +2002,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1455
2002
  process.exit(1);
1456
2003
  }
1457
2004
  });
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) => {
2005
+ 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(
2006
+ "after",
2007
+ dedent8`
2008
+
2009
+ Generates a template with all required fact columns pre-filled.
2010
+
2011
+ Examples:
2012
+ $ lexq analytics dataset template --group-id <gid> --version-id <vid> --output template.csv
2013
+ $ lexq analytics dataset template --group-id <gid> --version-id <vid> --format json
2014
+ `
2015
+ ).action(async (opts) => {
1459
2016
  try {
1460
2017
  const globalOpts = program.opts();
1461
2018
  const config = loadConfig();
@@ -1502,16 +2059,35 @@ function resolveBody(opts) {
1502
2059
 
1503
2060
  // src/commands/history.ts
1504
2061
  import "commander";
2062
+ import dedent9 from "dedent";
1505
2063
  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) => {
2064
+ const history = program.command("history").description("Execution history").addHelpText(
2065
+ "after",
2066
+ dedent9`
2067
+
2068
+ View and analyze policy execution logs from production traffic.
2069
+
2070
+ Commands:
2071
+ list List execution history with filters
2072
+ get Get full execution detail (request facts, traces, decisions)
2073
+ stats Aggregate statistics (success rate, latency, counts)
2074
+
2075
+ Statuses: SUCCESS, NO_MATCH, ERROR, TIMEOUT
2076
+ `
2077
+ );
2078
+ 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(
2079
+ "after",
2080
+ dedent9`
2081
+
2082
+ Examples:
2083
+ $ lexq history list --status ERROR --format table
2084
+ $ lexq history list --group-id <gid> --start-date 2026-04-01 --end-date 2026-04-15
2085
+ `
2086
+ ).action(async (opts) => {
1508
2087
  try {
1509
2088
  const globalOpts = program.opts();
1510
2089
  const format = globalOpts.format ?? "json";
1511
- const params = {
1512
- page: opts.page,
1513
- size: opts.size
1514
- };
2090
+ const params = { page: opts.page, size: opts.size };
1515
2091
  if (opts.traceId) params.traceId = opts.traceId;
1516
2092
  if (opts.groupId) params.policyGroupId = opts.groupId;
1517
2093
  if (opts.versionId) params.versionId = opts.versionId;
@@ -1553,7 +2129,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1553
2129
  process.exit(1);
1554
2130
  }
1555
2131
  });
1556
- history.command("get").description("Get execution detail").requiredOption("--id <traceId>", "Trace ID").action(async (opts) => {
2132
+ history.command("get").description("Get execution detail").requiredOption("--id <traceId>", "Trace ID").addHelpText(
2133
+ "after",
2134
+ dedent9`
2135
+
2136
+ Returns the full execution detail including request facts, result traces,
2137
+ and decision traces (SELECTED, BLOCKED_MUTEX, LOST_PRIORITY, etc.).
2138
+ `
2139
+ ).action(async (opts) => {
1557
2140
  try {
1558
2141
  const globalOpts = program.opts();
1559
2142
  const data = await apiRequest(
@@ -1572,7 +2155,17 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1572
2155
  process.exit(1);
1573
2156
  }
1574
2157
  });
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) => {
2158
+ 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(
2159
+ "after",
2160
+ dedent9`
2161
+
2162
+ Shows total executions, success/no-match/failure counts, success rate, and avg latency.
2163
+
2164
+ Example:
2165
+ $ lexq history stats --format table
2166
+ $ lexq history stats --group-id <gid> --start-date 2026-04-01
2167
+ `
2168
+ ).action(async (opts) => {
1576
2169
  try {
1577
2170
  const globalOpts = program.opts();
1578
2171
  const format = globalOpts.format ?? "json";
@@ -1613,8 +2206,25 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1613
2206
 
1614
2207
  // src/commands/integrations.ts
1615
2208
  import "commander";
2209
+ import dedent10 from "dedent";
1616
2210
  function registerIntegrationCommands(program) {
1617
- const integrations = program.command("integrations").description("Manage external integrations");
2211
+ const integrations = program.command("integrations").description("Manage external integrations").addHelpText(
2212
+ "after",
2213
+ dedent10`
2214
+
2215
+ Integrations connect rule actions to external services (webhooks, coupons,
2216
+ points, notifications, CRM, messengers).
2217
+
2218
+ Commands:
2219
+ list List all integrations
2220
+ get Get integration detail
2221
+ save Create or update an integration
2222
+ delete Delete an integration
2223
+ config-spec Show required configuration fields per type
2224
+
2225
+ Types: COUPON, POINT, NOTIFICATION, CRM, MESSENGER, WEBHOOK
2226
+ `
2227
+ );
1618
2228
  integrations.command("list").description("List integrations").option(
1619
2229
  "--type <type>",
1620
2230
  "Filter by type (COUPON, POINT, NOTIFICATION, CRM, MESSENGER, WEBHOOK)"
@@ -1622,10 +2232,7 @@ function registerIntegrationCommands(program) {
1622
2232
  try {
1623
2233
  const globalOpts = program.opts();
1624
2234
  const format = globalOpts.format ?? "json";
1625
- const params = {
1626
- page: opts.page,
1627
- size: opts.size
1628
- };
2235
+ const params = { page: opts.page, size: opts.size };
1629
2236
  if (opts.type) params.type = opts.type;
1630
2237
  const data = await apiRequest("GET", "integrations", {
1631
2238
  apiKey: globalOpts.apiKey,
@@ -1671,7 +2278,40 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1671
2278
  process.exit(1);
1672
2279
  }
1673
2280
  });
1674
- integrations.command("save").description("Create or update an integration").requiredOption("--json <body>", "Request body as JSON string").action(async (opts) => {
2281
+ integrations.command("save").description("Create or update an integration").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
2282
+ "after",
2283
+ dedent10`
2284
+
2285
+ Examples:
2286
+ # Create
2287
+ $ lexq integrations save --json '{
2288
+ "type": "WEBHOOK",
2289
+ "name": "Order Processing",
2290
+ "baseUrl": "https://api.example.com/webhooks/orders",
2291
+ "isActive": true
2292
+ }'
2293
+
2294
+ # Update (provide id)
2295
+ $ lexq integrations save --json '{
2296
+ "id": "<existing-id>",
2297
+ "type": "WEBHOOK",
2298
+ "name": "Order Processing (v2)",
2299
+ "baseUrl": "https://api.example.com/v2/webhooks/orders",
2300
+ "isActive": true
2301
+ }'
2302
+
2303
+ Fields:
2304
+ id string Provide to update, omit to create
2305
+ type string COUPON | POINT | NOTIFICATION | CRM | MESSENGER | WEBHOOK (required)
2306
+ name string Integration name (required, unique per tenant)
2307
+ baseUrl string Base URL of the external service (required)
2308
+ credential string API key or token (optional, write-only)
2309
+ additionalConfig object Extra config key-value pairs (optional)
2310
+ isActive boolean Enable/disable [default: true]
2311
+
2312
+ Use "lexq integrations config-spec" to see required fields per type.
2313
+ `
2314
+ ).action(async (opts) => {
1675
2315
  try {
1676
2316
  const globalOpts = program.opts();
1677
2317
  const body = JSON.parse(opts.json);
@@ -1688,7 +2328,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1688
2328
  process.exit(1);
1689
2329
  }
1690
2330
  });
1691
- integrations.command("delete").description("Delete an integration").requiredOption("--id <integrationId>", "Integration ID").option("--force", "Skip confirmation prompt").action(async (opts) => {
2331
+ integrations.command("delete").description("Delete an integration").requiredOption("--id <integrationId>", "Integration ID").option("--force", "Skip confirmation prompt").addHelpText(
2332
+ "after",
2333
+ dedent10`
2334
+
2335
+ Rules referencing this integration will fail at execution time.
2336
+ Use --force to skip confirmation.
2337
+ `
2338
+ ).action(async (opts) => {
1692
2339
  try {
1693
2340
  const globalOpts = program.opts();
1694
2341
  if (!opts.force) {
@@ -1713,7 +2360,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1713
2360
  process.exit(1);
1714
2361
  }
1715
2362
  });
1716
- integrations.command("config-spec").description("Get integration configuration field specs").action(async () => {
2363
+ integrations.command("config-spec").description("Get integration configuration field specs").addHelpText(
2364
+ "after",
2365
+ dedent10`
2366
+
2367
+ Shows required and optional configuration fields for each integration type.
2368
+
2369
+ Example:
2370
+ $ lexq integrations config-spec
2371
+ `
2372
+ ).action(async () => {
1717
2373
  try {
1718
2374
  const globalOpts = program.opts();
1719
2375
  const data = await apiRequest("GET", "integrations/config-spec", {
@@ -1732,6 +2388,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1732
2388
 
1733
2389
  // src/commands/logs.ts
1734
2390
  import "commander";
2391
+ import dedent11 from "dedent";
1735
2392
 
1736
2393
  // src/types/enums.ts
1737
2394
  var FailureStatus = ["PENDING", "RESOLVED", "IGNORED"];
@@ -1750,20 +2407,50 @@ var TaskType = [
1750
2407
  "WEBHOOK_EXECUTE",
1751
2408
  // Internal
1752
2409
  "IMAGE_PROCESSING",
1753
- "DAILY_SETTLEMENT"
2410
+ "DAILY_SETTLEMENT",
2411
+ "PLATFORM_WEBHOOK"
2412
+ ];
2413
+ var PlatformEventType = [
2414
+ "VERSION_PUBLISHED",
2415
+ "DEPLOYED",
2416
+ "ROLLED_BACK",
2417
+ "UNDEPLOYED"
1754
2418
  ];
2419
+ var WebhookPayloadFormat = ["GENERIC", "SLACK"];
1755
2420
 
1756
2421
  // src/commands/logs.ts
1757
2422
  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) => {
2423
+ const logs = program.command("logs").description("Failure logs").addHelpText(
2424
+ "after",
2425
+ dedent11`
2426
+
2427
+ System failure logs (DLQ) for background tasks — webhook calls, coupon issuance,
2428
+ point operations, notifications, and platform event webhooks.
2429
+
2430
+ Commands:
2431
+ list List failure logs with filters
2432
+ get Get failure log detail (includes payload for retry)
2433
+ action Process a single log (RETRY, IGNORE, RESOLVE)
2434
+ bulk-action Process multiple logs at once
2435
+
2436
+ Statuses: PENDING (needs attention), RESOLVED, IGNORED
2437
+ Categories: INTEGRATION (external), INTERNAL (system)
2438
+ `
2439
+ );
2440
+ 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(
2441
+ "after",
2442
+ dedent11`
2443
+
2444
+ Examples:
2445
+ $ lexq logs list --status PENDING --format table
2446
+ $ lexq logs list --task-type PLATFORM_WEBHOOK --category INTERNAL
2447
+ $ lexq logs list --keyword "timeout" --start-date 2026-04-01
2448
+ `
2449
+ ).action(async (opts) => {
1760
2450
  try {
1761
2451
  const globalOpts = program.opts();
1762
2452
  const format = globalOpts.format ?? "json";
1763
- const params = {
1764
- page: opts.page,
1765
- size: opts.size
1766
- };
2453
+ const params = { page: opts.page, size: opts.size };
1767
2454
  if (opts.category) params.category = opts.category;
1768
2455
  if (opts.taskType) params.taskType = opts.taskType;
1769
2456
  if (opts.status) params.status = opts.status;
@@ -1801,7 +2488,14 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1801
2488
  process.exit(1);
1802
2489
  }
1803
2490
  });
1804
- logs.command("get").description("Get failure log detail").requiredOption("--id <logId>", "Log ID").action(async (opts) => {
2491
+ logs.command("get").description("Get failure log detail").requiredOption("--id <logId>", "Log ID").addHelpText(
2492
+ "after",
2493
+ dedent11`
2494
+
2495
+ Includes the full payload that was used for the failed operation.
2496
+ Use this to inspect what went wrong before deciding to RETRY or RESOLVE.
2497
+ `
2498
+ ).action(async (opts) => {
1805
2499
  try {
1806
2500
  const globalOpts = program.opts();
1807
2501
  const data = await apiRequest("GET", `failure-logs/${opts.id}`, {
@@ -1816,7 +2510,19 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1816
2510
  process.exit(1);
1817
2511
  }
1818
2512
  });
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) => {
2513
+ 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(
2514
+ "after",
2515
+ dedent11`
2516
+
2517
+ Actions:
2518
+ RETRY Re-execute the failed operation with the original payload
2519
+ IGNORE Mark as intentionally skipped (won't appear in PENDING)
2520
+ RESOLVE Mark as manually resolved (e.g., fixed via external system)
2521
+
2522
+ Example:
2523
+ $ lexq logs action --id <logId> --action RETRY
2524
+ `
2525
+ ).action(async (opts) => {
1820
2526
  try {
1821
2527
  const globalOpts = program.opts();
1822
2528
  const data = await apiRequest(
@@ -1836,7 +2542,16 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1836
2542
  process.exit(1);
1837
2543
  }
1838
2544
  });
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) => {
2545
+ 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(
2546
+ "after",
2547
+ dedent11`
2548
+
2549
+ Processes each log individually. Failures are skipped with a warning.
2550
+
2551
+ Example:
2552
+ $ lexq logs bulk-action --ids "id1,id2,id3" --action RESOLVE
2553
+ `
2554
+ ).action(async (opts) => {
1840
2555
  try {
1841
2556
  const globalOpts = program.opts();
1842
2557
  const logIds = opts.ids.split(",").map((id) => id.trim());
@@ -1855,8 +2570,212 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
1855
2570
  });
1856
2571
  }
1857
2572
 
2573
+ // src/commands/webhook-subscriptions.ts
2574
+ import "commander";
2575
+ import dedent12 from "dedent";
2576
+ function registerWebhookSubscriptionCommands(program) {
2577
+ const webhooks = program.command("webhook-subscriptions").description("Manage platform event webhook subscriptions").addHelpText(
2578
+ "after",
2579
+ dedent12`
2580
+
2581
+ Receive notifications when deployment lifecycle events occur
2582
+ (publish, deploy, rollback, undeploy).
2583
+
2584
+ Commands:
2585
+ list List all webhook subscriptions
2586
+ get Get subscription detail
2587
+ save Create or update a subscription
2588
+ delete Delete a subscription
2589
+ test Send a test event to verify connectivity
2590
+
2591
+ This is separate from Integrations (rule action webhooks).
2592
+ Webhook subscriptions are for platform-level event notifications.
2593
+ `
2594
+ );
2595
+ webhooks.command("list").description("List webhook subscriptions").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
2596
+ try {
2597
+ const globalOpts = program.opts();
2598
+ const format = globalOpts.format ?? "json";
2599
+ const data = await apiRequest(
2600
+ "GET",
2601
+ "webhook-subscriptions",
2602
+ {
2603
+ apiKey: globalOpts.apiKey,
2604
+ baseUrl: globalOpts.baseUrl,
2605
+ dryRun: globalOpts.dryRun,
2606
+ verbose: globalOpts.verbose,
2607
+ params: { page: opts.page, size: opts.size }
2608
+ }
2609
+ );
2610
+ if (format === "table") {
2611
+ printTable(
2612
+ ["ID", "Name", "Events", "Format", "Active"],
2613
+ data.content.map((s) => [
2614
+ s.id.substring(0, 8),
2615
+ s.name,
2616
+ s.subscribedEvents.join(", "),
2617
+ s.payloadFormat,
2618
+ s.isActive ? "\u2713" : "\u2717"
2619
+ ]),
2620
+ { truncate: 32 }
2621
+ );
2622
+ console.log(`
2623
+ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
2624
+ } else {
2625
+ printJson(data);
2626
+ }
2627
+ } catch (error) {
2628
+ printError(error);
2629
+ process.exit(1);
2630
+ }
2631
+ });
2632
+ webhooks.command("get").description("Get webhook subscription detail").requiredOption("--id <subscriptionId>", "Subscription ID").action(async (opts) => {
2633
+ try {
2634
+ const globalOpts = program.opts();
2635
+ const data = await apiRequest(
2636
+ "GET",
2637
+ `webhook-subscriptions/${opts.id}`,
2638
+ {
2639
+ apiKey: globalOpts.apiKey,
2640
+ baseUrl: globalOpts.baseUrl,
2641
+ dryRun: globalOpts.dryRun,
2642
+ verbose: globalOpts.verbose
2643
+ }
2644
+ );
2645
+ printJson(data);
2646
+ } catch (error) {
2647
+ printError(error);
2648
+ process.exit(1);
2649
+ }
2650
+ });
2651
+ webhooks.command("save").description("Create or update a webhook subscription").requiredOption("--json <body>", "Request body as JSON string").addHelpText(
2652
+ "after",
2653
+ dedent12`
2654
+
2655
+ Examples:
2656
+ # Create (Slack format)
2657
+ $ lexq webhook-subscriptions save --json '{
2658
+ "name": "Deploy Alert",
2659
+ "webhookUrl": "https://hooks.slack.com/services/...",
2660
+ "subscribedEvents": ["DEPLOYED", "ROLLED_BACK"],
2661
+ "payloadFormat": "SLACK"
2662
+ }'
2663
+
2664
+ # Update (provide id)
2665
+ $ lexq webhook-subscriptions save --json '{
2666
+ "id": "<existing-id>",
2667
+ "name": "Deploy Alert",
2668
+ "webhookUrl": "https://hooks.slack.com/services/...",
2669
+ "subscribedEvents": ["VERSION_PUBLISHED", "DEPLOYED", "ROLLED_BACK", "UNDEPLOYED"],
2670
+ "payloadFormat": "SLACK",
2671
+ "secret": "my-hmac-secret"
2672
+ }'
2673
+
2674
+ Fields:
2675
+ name string Subscription name (required, unique per tenant)
2676
+ webhookUrl string Webhook endpoint URL (required)
2677
+ subscribedEvents string[] VERSION_PUBLISHED, DEPLOYED, ROLLED_BACK, UNDEPLOYED
2678
+ payloadFormat string GENERIC (full JSON) or SLACK ({"text":"..."}) [default: GENERIC]
2679
+ secret string HMAC-SHA256 signing secret (optional)
2680
+ isActive boolean Enable/disable (optional) [default: true]
2681
+ id string Provide to update, omit to create
2682
+
2683
+ When secret is set, an X-LexQ-Signature header (sha256=hex) is sent for verification.
2684
+ `
2685
+ ).action(async (opts) => {
2686
+ try {
2687
+ const globalOpts = program.opts();
2688
+ const body = JSON.parse(opts.json);
2689
+ const data = await apiRequest(
2690
+ "POST",
2691
+ "webhook-subscriptions",
2692
+ {
2693
+ apiKey: globalOpts.apiKey,
2694
+ baseUrl: globalOpts.baseUrl,
2695
+ dryRun: globalOpts.dryRun,
2696
+ verbose: globalOpts.verbose,
2697
+ body
2698
+ }
2699
+ );
2700
+ printJson(data);
2701
+ } catch (error) {
2702
+ printError(error);
2703
+ process.exit(1);
2704
+ }
2705
+ });
2706
+ webhooks.command("delete").description("Delete a webhook subscription").requiredOption("--id <subscriptionId>", "Subscription ID").option("--force", "Skip confirmation prompt").addHelpText(
2707
+ "after",
2708
+ dedent12`
2709
+
2710
+ Use --force to skip the confirmation prompt.
2711
+
2712
+ Example:
2713
+ $ lexq webhook-subscriptions delete --id <id> --force
2714
+ `
2715
+ ).action(async (opts) => {
2716
+ try {
2717
+ const globalOpts = program.opts();
2718
+ if (!opts.force) {
2719
+ const { createInterface: createInterface2 } = await import("readline/promises");
2720
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
2721
+ const answer = await rl.question(`Delete webhook subscription ${opts.id}? [y/N] `);
2722
+ rl.close();
2723
+ if (answer.toLowerCase() !== "y") {
2724
+ console.log("Cancelled.");
2725
+ return;
2726
+ }
2727
+ }
2728
+ await apiRequest("DELETE", `webhook-subscriptions/${opts.id}`, {
2729
+ apiKey: globalOpts.apiKey,
2730
+ baseUrl: globalOpts.baseUrl,
2731
+ dryRun: globalOpts.dryRun,
2732
+ verbose: globalOpts.verbose
2733
+ });
2734
+ console.log(`\u2713 Webhook subscription ${opts.id} deleted.`);
2735
+ } catch (error) {
2736
+ printError(error);
2737
+ process.exit(1);
2738
+ }
2739
+ });
2740
+ webhooks.command("test").description("Send a test event to verify webhook connectivity").requiredOption("--id <subscriptionId>", "Subscription ID").addHelpText(
2741
+ "after",
2742
+ dedent12`
2743
+
2744
+ Sends a test event to the webhook URL and reports the HTTP status code.
2745
+ Does not record failures in the failure log.
2746
+
2747
+ The response includes:
2748
+ statusCode HTTP status code returned by the webhook endpoint
2749
+ success true if 2xx response received
2750
+ message human-readable status message
2751
+
2752
+ Example:
2753
+ $ lexq webhook-subscriptions test --id <id>
2754
+ `
2755
+ ).action(async (opts) => {
2756
+ try {
2757
+ const globalOpts = program.opts();
2758
+ const data = await apiRequest(
2759
+ "POST",
2760
+ `webhook-subscriptions/${opts.id}/test`,
2761
+ {
2762
+ apiKey: globalOpts.apiKey,
2763
+ baseUrl: globalOpts.baseUrl,
2764
+ dryRun: globalOpts.dryRun,
2765
+ verbose: globalOpts.verbose
2766
+ }
2767
+ );
2768
+ printJson(data);
2769
+ } catch (error) {
2770
+ printError(error);
2771
+ process.exit(1);
2772
+ }
2773
+ });
2774
+ }
2775
+
1858
2776
  // src/commands/serve.ts
1859
2777
  import "commander";
2778
+ import dedent15 from "dedent";
1860
2779
 
1861
2780
  // src/mcp/server.ts
1862
2781
  import { readFileSync as readFileSync3 } from "fs";
@@ -1996,7 +2915,7 @@ function registerGroupTools(server, callApi) {
1996
2915
  "lexq_groups_update",
1997
2916
  {
1998
2917
  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.",
2918
+ description: "Update a policy group. Only provided fields are updated; omitted fields remain unchanged.",
2000
2919
  inputSchema: {
2001
2920
  groupId: z.string().uuid().describe("Policy group ID"),
2002
2921
  name: z.string().optional().describe("New name"),
@@ -2093,10 +3012,10 @@ function registerVersionTools(server, callApi) {
2093
3012
  "lexq_versions_create",
2094
3013
  {
2095
3014
  title: "Create Policy Version",
2096
- description: "Create a new DRAFT version in a policy group. Provide a commit message and optional effective date range.",
3015
+ description: "Create a new DRAFT version in a policy group. Optionally provide a commit message and effective date range.",
2097
3016
  inputSchema: {
2098
3017
  groupId: z2.string().uuid().describe("Policy group ID"),
2099
- commitMessage: z2.string().describe("Commit message describing this version"),
3018
+ commitMessage: z2.string().optional().describe("Commit message describing this version"),
2100
3019
  effectiveFrom: z2.string().optional().describe("Effective start date (ISO 8601)"),
2101
3020
  effectiveTo: z2.string().optional().describe("Effective end date (ISO 8601)")
2102
3021
  }
@@ -2107,7 +3026,7 @@ function registerVersionTools(server, callApi) {
2107
3026
  "lexq_versions_update",
2108
3027
  {
2109
3028
  title: "Update Policy Version",
2110
- description: "Update a DRAFT version. Only DRAFT versions can be modified.",
3029
+ description: "Update a DRAFT version. Only DRAFT versions can be modified. Only provided fields are changed.",
2111
3030
  inputSchema: {
2112
3031
  groupId: z2.string().uuid().describe("Policy group ID"),
2113
3032
  versionId: z2.string().uuid().describe("Version ID"),
@@ -2150,6 +3069,7 @@ function registerVersionTools(server, callApi) {
2150
3069
 
2151
3070
  // src/mcp/tools/rules.ts
2152
3071
  import { z as z3 } from "zod";
3072
+ import dedent13 from "dedent";
2153
3073
  function registerRuleTools(server, callApi) {
2154
3074
  server.registerTool(
2155
3075
  "lexq_rules_list",
@@ -2184,32 +3104,34 @@ function registerRuleTools(server, callApi) {
2184
3104
  "lexq_rules_create",
2185
3105
  {
2186
3106
  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"|"KAKAO", 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 }`,
3107
+ description: dedent13`
3108
+ Create a rule in a DRAFT version. Requires name, priority, condition tree, and actions array.
3109
+
3110
+ Before creating rules with new fact keys, call lexq_facts_list to check existing facts.
3111
+ If a required key is missing, ask the user to confirm the type, isRequired, and description
3112
+ before calling lexq_facts_create registering facts enables type validation, Console UI
3113
+ autocomplete, and the dry-run requirements analyzer.
3114
+
3115
+ Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
3116
+ Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
3117
+ Value types: STRING, NUMBER, BOOLEAN, LIST_STRING, LIST_NUMBER
3118
+
3119
+ Actions: [{ type, parameters }]
3120
+
3121
+ Action parameter schemas:
3122
+ - DISCOUNT: { refVar: string, method: "PERCENTAGE"|"AMOUNT", rate?: number (when PERCENTAGE), value?: number (when AMOUNT) }
3123
+ - POINT: { refVar: string, targetVar: string, method: "PERCENTAGE"|"AMOUNT", rate?: number (when PERCENTAGE), value?: number (when AMOUNT), integrationId: uuid }
3124
+ - COUPON_ISSUE: { couponId: string, integrationId: uuid }
3125
+ - BLOCK: { reason: string }
3126
+ - NOTIFICATION: { channel: "SMS"|"EMAIL"|"PUSH", targetVar: string, templateId: string, integrationId: uuid }
3127
+ - 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).
3128
+ Platform examples:
3129
+ Slack: { "text": "Rule {{ruleName}} fired {{fact.customer_tier}}" }
3130
+ Discord: { "content": "Rule {{ruleName}} fired {{fact.customer_tier}}" }
3131
+ Generic: { "event": "rule_matched", "rule": "{{ruleName}}", "amount": "{{output.payment_amount}}" }
3132
+ - SET_FACT: { key: string, value: string|number|boolean }
3133
+ - ADD_TAG: { tag: string, targetVar: string }
3134
+ `,
2213
3135
  inputSchema: {
2214
3136
  groupId: z3.string().uuid().describe("Policy group ID"),
2215
3137
  versionId: z3.string().uuid().describe("Version ID"),
@@ -2342,12 +3264,12 @@ function registerFactTools(server, callApi) {
2342
3264
  "lexq_facts_update",
2343
3265
  {
2344
3266
  title: "Update Fact Definition",
2345
- description: "Update a fact definition. Key and type cannot be changed. System facts only allow name and description changes.",
3267
+ 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
3268
  inputSchema: {
2347
3269
  factId: z4.string().uuid().describe("Fact definition ID"),
2348
- name: z4.string().describe("Display name"),
3270
+ name: z4.string().optional().describe("Display name"),
2349
3271
  description: z4.string().optional().describe("Description"),
2350
- isRequired: z4.boolean().describe("Required flag")
3272
+ isRequired: z4.boolean().optional().describe("Required flag")
2351
3273
  }
2352
3274
  },
2353
3275
  async ({ factId, ...body }) => callApi("PUT", `schema/facts/${factId}`, { body })
@@ -2363,6 +3285,15 @@ function registerFactTools(server, callApi) {
2363
3285
  },
2364
3286
  async ({ factId }) => callApi("DELETE", `schema/facts/${factId}`)
2365
3287
  );
3288
+ server.registerTool(
3289
+ "lexq_facts_action_metadata",
3290
+ {
3291
+ title: "Get Action Runtime Fact Metadata",
3292
+ 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.",
3293
+ inputSchema: {}
3294
+ },
3295
+ async () => callApi("GET", "schema/action-metadata")
3296
+ );
2366
3297
  }
2367
3298
 
2368
3299
  // src/mcp/tools/deploy.ts
@@ -2376,7 +3307,7 @@ function registerDeployTools(server, callApi) {
2376
3307
  inputSchema: {
2377
3308
  groupId: z5.string().uuid().describe("Policy group ID"),
2378
3309
  versionId: z5.string().uuid().describe("Version ID to publish"),
2379
- memo: z5.string().min(1).describe("Publish Deployment memo (required)")
3310
+ memo: z5.string().min(1).describe("Publish memo (required)")
2380
3311
  }
2381
3312
  },
2382
3313
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/versions/${versionId}/publish`, { body: { memo } })
@@ -2389,7 +3320,7 @@ function registerDeployTools(server, callApi) {
2389
3320
  inputSchema: {
2390
3321
  groupId: z5.string().uuid().describe("Policy group ID"),
2391
3322
  versionId: z5.string().uuid().describe("Version ID to deploy"),
2392
- memo: z5.string().min(1).describe("Live Deployment memo (required)")
3323
+ memo: z5.string().min(1).describe("Deployment memo (required)")
2393
3324
  }
2394
3325
  },
2395
3326
  async ({ groupId, versionId, memo }) => callApi("POST", `policy-groups/${groupId}/deploy`, {
@@ -2403,7 +3334,7 @@ function registerDeployTools(server, callApi) {
2403
3334
  description: "Rollback to the previous deployed version. Only available if there is a previous version.",
2404
3335
  inputSchema: {
2405
3336
  groupId: z5.string().uuid().describe("Policy group ID"),
2406
- memo: z5.string().default("").describe("Rollback reason")
3337
+ memo: z5.string().min(1).describe("Rollback reason (required)")
2407
3338
  }
2408
3339
  },
2409
3340
  async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/rollback`, {
@@ -2417,7 +3348,7 @@ function registerDeployTools(server, callApi) {
2417
3348
  description: "Remove the live version from traffic. The version stays ACTIVE but no longer serves requests.",
2418
3349
  inputSchema: {
2419
3350
  groupId: z5.string().uuid().describe("Policy group ID"),
2420
- memo: z5.string().default("").describe("Undeploy reason")
3351
+ memo: z5.string().min(1).describe("Undeploy reason (required)")
2421
3352
  }
2422
3353
  },
2423
3354
  async ({ groupId, memo }) => callApi("POST", `policy-groups/${groupId}/undeploy`, {
@@ -2433,13 +3364,19 @@ function registerDeployTools(server, callApi) {
2433
3364
  page: z5.number().int().min(0).default(0).describe("Page number"),
2434
3365
  size: z5.number().int().min(1).max(100).default(20).describe("Page size"),
2435
3366
  groupId: z5.string().uuid().optional().describe("Filter by group ID"),
2436
- type: z5.enum(["PUBLISH", "DEPLOY", "ROLLBACK", "UNDEPLOY"]).optional().describe("Filter by deployment type")
3367
+ types: z5.string().optional().describe(
3368
+ "Filter by deployment types (comma-separated: PUBLISH,DEPLOY,ROLLBACK,UNDEPLOY)"
3369
+ ),
3370
+ startDate: z5.string().optional().describe("Start date (yyyy-MM-dd)"),
3371
+ endDate: z5.string().optional().describe("End date (yyyy-MM-dd)")
2437
3372
  }
2438
3373
  },
2439
- async ({ page, size, groupId, type }) => {
3374
+ async ({ page, size, groupId, types, startDate, endDate }) => {
2440
3375
  const params = paginationParams(page, size);
2441
3376
  if (groupId) params.groupId = groupId;
2442
- if (type) params.type = type;
3377
+ if (types) params.types = types;
3378
+ if (startDate) params.startDate = startDate;
3379
+ if (endDate) params.endDate = endDate;
2443
3380
  return callApi("GET", "deployments", { params });
2444
3381
  }
2445
3382
  );
@@ -2463,18 +3400,46 @@ function registerDeployTools(server, callApi) {
2463
3400
  },
2464
3401
  async () => callApi("GET", "deployments/overview")
2465
3402
  );
3403
+ server.registerTool(
3404
+ "lexq_deploy_deployable",
3405
+ {
3406
+ title: "List Deployable Versions",
3407
+ description: "List ACTIVE (published) versions that can be deployed for a group. Use this to find which versions are available before calling deploy live.",
3408
+ inputSchema: {
3409
+ groupId: z5.string().uuid().describe("Policy group ID")
3410
+ }
3411
+ },
3412
+ async ({ groupId }) => callApi("GET", `deployments/groups/${groupId}/deployable-versions`)
3413
+ );
3414
+ server.registerTool(
3415
+ "lexq_deploy_diff",
3416
+ {
3417
+ title: "Deployment Diff",
3418
+ description: "Compare rule snapshots between two versions. Shows added, removed, and modified rules. Useful for reviewing changes before deploying a new version.",
3419
+ inputSchema: {
3420
+ baseVersionId: z5.string().uuid().describe("Base version ID (typically the current live)"),
3421
+ targetVersionId: z5.string().uuid().describe("Target version ID (the one you want to deploy)")
3422
+ }
3423
+ },
3424
+ async ({ baseVersionId, targetVersionId }) => callApi("GET", "deployments/diff", {
3425
+ params: { baseVersionId, targetVersionId }
3426
+ })
3427
+ );
2466
3428
  }
2467
3429
 
2468
3430
  // src/mcp/tools/analytics.ts
2469
3431
  import { z as z6 } from "zod";
3432
+ import dedent14 from "dedent";
2470
3433
  function registerAnalyticsTools(server, callApi) {
2471
3434
  server.registerTool(
2472
3435
  "lexq_dry_run",
2473
3436
  {
2474
3437
  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.`,
3438
+ description: dedent14`
3439
+ Execute a single dry run against a version. Tests how rules evaluate given input facts without side effects.
3440
+ Example input: { "facts": { "payment_amount": 100000, "customer_tier": "VIP" } }
3441
+ Always dry-run before publishing to validate rule behavior.
3442
+ `,
2478
3443
  inputSchema: {
2479
3444
  versionId: z6.string().uuid().describe("Policy version ID to test against"),
2480
3445
  facts: z6.string().describe('JSON string of facts object, e.g. {"payment_amount":100000}'),
@@ -2523,21 +3488,23 @@ function registerAnalyticsTools(server, callApi) {
2523
3488
  "lexq_simulation_start",
2524
3489
  {
2525
3490
  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
- }`,
3491
+ description: dedent14`
3492
+ Start a batch simulation against historical or uploaded data.
3493
+
3494
+ dataset.type: "HISTORICAL" or "UPLOADED"
3495
+ dataset.source (when HISTORICAL): "EXECUTION_LOGS"
3496
+ dataset.from / dataset.to: date range (yyyy-MM-dd, when HISTORICAL)
3497
+ options.maxRecords: number (max 100000, default 10000)
3498
+ options.baselinePolicyVersionId: uuid (optional, for comparison)
3499
+ options.includeRuleStats: boolean
3500
+
3501
+ Example body:
3502
+ {
3503
+ "policyVersionId": "<uuid>",
3504
+ "dataset": { "type": "HISTORICAL", "source": "EXECUTION_LOGS", "from": "2025-01-01", "to": "2025-01-31" },
3505
+ "options": { "baselinePolicyVersionId": "<uuid>", "includeRuleStats": true, "maxRecords": 10000 }
3506
+ }
3507
+ `,
2541
3508
  inputSchema: {
2542
3509
  body: z6.string().describe("JSON string of SimulationRequest")
2543
3510
  }
@@ -2608,16 +3575,18 @@ function registerAnalyticsTools(server, callApi) {
2608
3575
  "lexq_dataset_upload",
2609
3576
  {
2610
3577
  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}]`,
3578
+ description: dedent14`
3579
+ Upload inline CSV or JSON content as a simulation dataset.
3580
+ The content is uploaded to S3 and a path is returned.
3581
+ Use this path in simulation start with dataset type UPLOADED.
3582
+
3583
+ CSV example:
3584
+ user_id,payment_amount
3585
+ user_001,150000
3586
+ user_002,50000
3587
+
3588
+ JSON example: [{"user_id":"user_001","payment_amount":150000}, {"user_id":"user_002","payment_amount":50000}]
3589
+ `,
2621
3590
  inputSchema: {
2622
3591
  content: z6.string().describe("CSV or JSON content as string"),
2623
3592
  filename: z6.string().default("dataset.csv").describe("Filename with extension (.csv or .json)")
@@ -2825,14 +3794,14 @@ function registerLogTools(server, callApi) {
2825
3794
  "lexq_logs_action",
2826
3795
  {
2827
3796
  title: "Process Failure Log",
2828
- description: "Process a single failure log: RETRY, RESOLVE, or IGNORE.",
3797
+ description: "Process a single failure log: RETRY (re-execute with original payload), RESOLVE (mark as manually fixed), or IGNORE (skip intentionally).",
2829
3798
  inputSchema: {
2830
3799
  logId: z9.string().uuid().describe("Failure log ID"),
2831
3800
  action: z9.enum(FailureAction).describe("Action to take")
2832
3801
  }
2833
3802
  },
2834
3803
  async ({ logId, action }) => callApi("POST", `failure-logs/${logId}/actions`, {
2835
- body: { action }
3804
+ params: { action }
2836
3805
  })
2837
3806
  );
2838
3807
  server.registerTool(
@@ -2851,6 +3820,76 @@ function registerLogTools(server, callApi) {
2851
3820
  );
2852
3821
  }
2853
3822
 
3823
+ // src/mcp/tools/webhook-subscriptions.ts
3824
+ import { z as z10 } from "zod";
3825
+ function registerWebhookSubscriptionTools(server, callApi) {
3826
+ server.registerTool(
3827
+ "lexq_webhook_subscriptions_list",
3828
+ {
3829
+ title: "List Webhook Subscriptions",
3830
+ description: "List platform event webhook subscriptions. These receive deployment lifecycle notifications (publish, deploy, rollback, undeploy).",
3831
+ inputSchema: {
3832
+ page: z10.number().int().min(0).default(0).describe("Page number"),
3833
+ size: z10.number().int().min(1).max(100).default(20).describe("Page size")
3834
+ }
3835
+ },
3836
+ async ({ page, size }) => {
3837
+ const params = paginationParams(page, size);
3838
+ return callApi("GET", "webhook-subscriptions", { params });
3839
+ }
3840
+ );
3841
+ server.registerTool(
3842
+ "lexq_webhook_subscriptions_get",
3843
+ {
3844
+ title: "Get Webhook Subscription",
3845
+ description: "Get webhook subscription detail by ID.",
3846
+ inputSchema: {
3847
+ id: z10.string().uuid().describe("Webhook subscription ID")
3848
+ }
3849
+ },
3850
+ async ({ id }) => callApi("GET", `webhook-subscriptions/${id}`)
3851
+ );
3852
+ server.registerTool(
3853
+ "lexq_webhook_subscriptions_save",
3854
+ {
3855
+ title: "Save Webhook Subscription",
3856
+ 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": "..."}).',
3857
+ inputSchema: {
3858
+ id: z10.string().uuid().optional().describe("Subscription ID (omit to create, provide to update)"),
3859
+ name: z10.string().min(1).describe("Subscription name (unique per tenant)"),
3860
+ webhookUrl: z10.string().url().describe("Webhook endpoint URL"),
3861
+ subscribedEvents: z10.array(z10.enum(PlatformEventType)).min(1).describe("Events to subscribe to"),
3862
+ payloadFormat: z10.enum(WebhookPayloadFormat).optional().default("GENERIC").describe("Payload format"),
3863
+ secret: z10.string().optional().describe("HMAC-SHA256 signing secret"),
3864
+ isActive: z10.boolean().optional().default(true).describe("Whether the subscription is active")
3865
+ }
3866
+ },
3867
+ async ({ ...body }) => callApi("POST", "webhook-subscriptions", { body })
3868
+ );
3869
+ server.registerTool(
3870
+ "lexq_webhook_subscriptions_delete",
3871
+ {
3872
+ title: "Delete Webhook Subscription",
3873
+ description: "Delete a webhook subscription by ID.",
3874
+ inputSchema: {
3875
+ id: z10.string().uuid().describe("Webhook subscription ID")
3876
+ }
3877
+ },
3878
+ async ({ id }) => callApi("DELETE", `webhook-subscriptions/${id}`)
3879
+ );
3880
+ server.registerTool(
3881
+ "lexq_webhook_subscriptions_test",
3882
+ {
3883
+ title: "Test Webhook Subscription",
3884
+ description: "Send a test event to verify webhook connectivity. Returns the HTTP status code and success/failure message.",
3885
+ inputSchema: {
3886
+ id: z10.string().uuid().describe("Webhook subscription ID")
3887
+ }
3888
+ },
3889
+ async ({ id }) => callApi("POST", `webhook-subscriptions/${id}/test`)
3890
+ );
3891
+ }
3892
+
2854
3893
  // src/mcp/register.ts
2855
3894
  function registerAllTools(server, callApi) {
2856
3895
  registerStatusTools(server, callApi);
@@ -2863,6 +3902,7 @@ function registerAllTools(server, callApi) {
2863
3902
  registerHistoryTools(server, callApi);
2864
3903
  registerIntegrationTools(server, callApi);
2865
3904
  registerLogTools(server, callApi);
3905
+ registerWebhookSubscriptionTools(server, callApi);
2866
3906
  }
2867
3907
 
2868
3908
  // src/mcp/server.ts
@@ -2888,7 +3928,27 @@ async function startMcpServer() {
2888
3928
 
2889
3929
  // src/commands/serve.ts
2890
3930
  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) => {
3931
+ 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(
3932
+ "after",
3933
+ dedent15`
3934
+
3935
+ Example:
3936
+ $ lexq serve --mcp
3937
+
3938
+ Starts a stdio MCP server that exposes 60 tools for policy management.
3939
+ Used by Claude Desktop, Claude.ai, Cursor, and other MCP-compatible clients.
3940
+
3941
+ Claude Desktop config (~/.claude/claude_desktop_config.json):
3942
+ {
3943
+ "mcpServers": {
3944
+ "lexq": {
3945
+ "command": "npx",
3946
+ "args": ["-y", "@lexq/cli", "serve", "--mcp"]
3947
+ }
3948
+ }
3949
+ }
3950
+ `
3951
+ ).action(async (opts) => {
2892
3952
  if (!opts.mcp) {
2893
3953
  console.error(
2894
3954
  "Error: --mcp flag is required.\nUsage: lexq serve --mcp\n\nStarts a stdio MCP server for Claude Desktop, Claude.ai, Gemini, etc."
@@ -2923,6 +3983,7 @@ function createCli() {
2923
3983
  registerHistoryCommands(program);
2924
3984
  registerIntegrationCommands(program);
2925
3985
  registerLogCommands(program);
3986
+ registerWebhookSubscriptionCommands(program);
2926
3987
  registerServeCommand(program);
2927
3988
  return program;
2928
3989
  }