@lexq/cli 0.1.18 → 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/AGENTS.md +60 -21
- package/CONTEXT.md +110 -49
- package/README.md +44 -37
- package/dist/index.js +1224 -163
- package/dist/mcp/register.d.ts +1 -1
- package/dist/mcp/register.js +196 -67
- package/package.json +3 -1
- package/skills/lexq-shared/SKILL.md +48 -42
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
|
-
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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
|
-
|
|
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)").
|
|
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)").
|
|
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)").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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 <
|
|
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 <
|
|
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").
|
|
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").
|
|
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
|
-
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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").
|
|
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
|
-
|
|
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").
|
|
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").
|
|
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
|
-
|
|
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").
|
|
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").
|
|
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").
|
|
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
|
-
|
|
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").
|
|
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
|
-
|
|
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").
|
|
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)").
|
|
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").
|
|
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").
|
|
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").
|
|
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
|
-
|
|
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").
|
|
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").
|
|
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").
|
|
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.
|
|
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.
|
|
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: `
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
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
|
|
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("
|
|
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().
|
|
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().
|
|
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
|
-
|
|
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,
|
|
3374
|
+
async ({ page, size, groupId, types, startDate, endDate }) => {
|
|
2440
3375
|
const params = paginationParams(page, size);
|
|
2441
3376
|
if (groupId) params.groupId = groupId;
|
|
2442
|
-
if (
|
|
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: `
|
|
2476
|
-
|
|
2477
|
-
|
|
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: `
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
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: `
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
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
|
-
|
|
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").
|
|
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
|
}
|