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