@lexq/cli 0.1.31 → 0.1.33
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/CONTEXT.md +1 -1
- package/dist/index.js +162 -49
- package/dist/mcp/register.d.ts +36 -2
- package/dist/mcp/register.js +35 -3
- package/package.json +1 -1
- package/skills/lexq-simulation/SKILL.md +1 -1
package/CONTEXT.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -59,7 +59,7 @@ var ApiError = class extends Error {
|
|
|
59
59
|
statusCode;
|
|
60
60
|
errorCode;
|
|
61
61
|
};
|
|
62
|
-
async function
|
|
62
|
+
async function doFetch(method, path, options) {
|
|
63
63
|
const config = loadConfig();
|
|
64
64
|
const baseUrl = options.baseUrl ?? config.baseUrl;
|
|
65
65
|
const apiKey = options.apiKey ?? config.apiKey;
|
|
@@ -69,18 +69,11 @@ async function apiRequest(method, path, options = {}) {
|
|
|
69
69
|
const url = new URL(path, baseUrl.endsWith("/") ? baseUrl : baseUrl + "/");
|
|
70
70
|
if (options.params) {
|
|
71
71
|
for (const [key, value] of Object.entries(options.params)) {
|
|
72
|
-
if (value !== void 0 && value !== "")
|
|
73
|
-
url.searchParams.set(key, value);
|
|
74
|
-
}
|
|
72
|
+
if (value !== void 0 && value !== "") url.searchParams.set(key, value);
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
|
-
const headers = {
|
|
78
|
-
|
|
79
|
-
Accept: "application/json"
|
|
80
|
-
};
|
|
81
|
-
if (options.body) {
|
|
82
|
-
headers["Content-Type"] = "application/json";
|
|
83
|
-
}
|
|
75
|
+
const headers = { "X-API-KEY": apiKey, Accept: "application/json" };
|
|
76
|
+
if (options.body) headers["Content-Type"] = "application/json";
|
|
84
77
|
if (options.dryRun) {
|
|
85
78
|
const masked = apiKey.length > 8 ? apiKey.substring(0, 4) + "****" + apiKey.substring(apiKey.length - 4) : "****";
|
|
86
79
|
console.log(`${method} ${url.toString()}`);
|
|
@@ -94,9 +87,7 @@ async function apiRequest(method, path, options = {}) {
|
|
|
94
87
|
console.log("\n(Use without --dry-run to execute)");
|
|
95
88
|
process.exit(0);
|
|
96
89
|
}
|
|
97
|
-
if (options.verbose) {
|
|
98
|
-
console.error(`\u2192 ${method} ${url.toString()}`);
|
|
99
|
-
}
|
|
90
|
+
if (options.verbose) console.error(`\u2192 ${method} ${url.toString()}`);
|
|
100
91
|
const startTime = Date.now();
|
|
101
92
|
const response = await fetch(url.toString(), {
|
|
102
93
|
method,
|
|
@@ -106,14 +97,9 @@ async function apiRequest(method, path, options = {}) {
|
|
|
106
97
|
if (options.verbose) {
|
|
107
98
|
console.error(`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`);
|
|
108
99
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
if (response.status === 204 || contentType === "") {
|
|
114
|
-
return void 0;
|
|
115
|
-
}
|
|
116
|
-
const json = await response.json();
|
|
100
|
+
return response;
|
|
101
|
+
}
|
|
102
|
+
function assertOk(response, json) {
|
|
117
103
|
if (!response.ok || json.result !== "SUCCESS") {
|
|
118
104
|
throw new ApiError(
|
|
119
105
|
response.status,
|
|
@@ -121,7 +107,23 @@ async function apiRequest(method, path, options = {}) {
|
|
|
121
107
|
json.message ?? `Request failed with status ${response.status}`
|
|
122
108
|
);
|
|
123
109
|
}
|
|
124
|
-
|
|
110
|
+
}
|
|
111
|
+
async function apiRequest(method, path, options = {}) {
|
|
112
|
+
const { data } = await apiRequestWithMeta(method, path, options);
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
async function apiRequestWithMeta(method, path, options = {}) {
|
|
116
|
+
const response = await doFetch(method, path, options);
|
|
117
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
118
|
+
if (contentType.includes("text/csv") || contentType.includes("application/octet-stream")) {
|
|
119
|
+
return { data: response, meta: null };
|
|
120
|
+
}
|
|
121
|
+
if (response.status === 204 || contentType === "") {
|
|
122
|
+
return { data: void 0, meta: null };
|
|
123
|
+
}
|
|
124
|
+
const json = await response.json();
|
|
125
|
+
assertOk(response, json);
|
|
126
|
+
return { data: json.data, meta: json.meta ?? null };
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
// src/lib/output.ts
|
|
@@ -163,6 +165,23 @@ function printError(error) {
|
|
|
163
165
|
console.error(JSON.stringify({ error: "UNKNOWN", message: String(error) }, null, 2));
|
|
164
166
|
}
|
|
165
167
|
}
|
|
168
|
+
function printUnregisteredFactsWarning(facts) {
|
|
169
|
+
if (facts.length === 0) return;
|
|
170
|
+
const s = facts.length === 1 ? "" : "s";
|
|
171
|
+
console.error(
|
|
172
|
+
`
|
|
173
|
+
\u26A0 ${facts.length} undefined fact${s} referenced \u2014 register to enable validation:`
|
|
174
|
+
);
|
|
175
|
+
for (const f of facts) {
|
|
176
|
+
const type = f.inferredType ?? f.candidateTypes?.[0] ?? "STRING";
|
|
177
|
+
const note = f.conflict ? ` (ambiguous: ${f.candidateTypes?.join(" | ") ?? "?"})` : "";
|
|
178
|
+
console.error(` \u2022 ${f.key} \u2192 ${type}${note}`);
|
|
179
|
+
console.error(
|
|
180
|
+
` lexq facts create --key ${f.key} --name "${f.suggestedName}" --type ${type}`
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
console.error("");
|
|
184
|
+
}
|
|
166
185
|
|
|
167
186
|
// src/commands/auth.ts
|
|
168
187
|
function registerAuthCommands(program) {
|
|
@@ -435,7 +454,7 @@ ${data.length} total`);
|
|
|
435
454
|
const answer = await rl.question(`Delete group ${opts.id}? [y/N] `);
|
|
436
455
|
rl.close();
|
|
437
456
|
if (answer.toLowerCase() !== "y") {
|
|
438
|
-
console.log("
|
|
457
|
+
console.log("Canceled.");
|
|
439
458
|
return;
|
|
440
459
|
}
|
|
441
460
|
}
|
|
@@ -543,7 +562,7 @@ ${data.length} total`);
|
|
|
543
562
|
const answer = await rl.question(`Stop A/B test for group ${opts.groupId}? [y/N] `);
|
|
544
563
|
rl.close();
|
|
545
564
|
if (answer.toLowerCase() !== "y") {
|
|
546
|
-
console.log("
|
|
565
|
+
console.log("Canceled.");
|
|
547
566
|
return;
|
|
548
567
|
}
|
|
549
568
|
}
|
|
@@ -755,7 +774,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
755
774
|
const answer = await rl.question(`Delete version ${opts.id}? [y/N] `);
|
|
756
775
|
rl.close();
|
|
757
776
|
if (answer.toLowerCase() !== "y") {
|
|
758
|
-
console.log("
|
|
777
|
+
console.log("Canceled.");
|
|
759
778
|
return;
|
|
760
779
|
}
|
|
761
780
|
}
|
|
@@ -935,7 +954,7 @@ ${data.length} total`);
|
|
|
935
954
|
try {
|
|
936
955
|
const globalOpts = program.opts();
|
|
937
956
|
const body = JSON.parse(opts.json);
|
|
938
|
-
const data = await
|
|
957
|
+
const { data, meta } = await apiRequestWithMeta(
|
|
939
958
|
"POST",
|
|
940
959
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/rules`,
|
|
941
960
|
{
|
|
@@ -947,6 +966,7 @@ ${data.length} total`);
|
|
|
947
966
|
}
|
|
948
967
|
);
|
|
949
968
|
printJson(data);
|
|
969
|
+
printUnregisteredFactsWarning(meta?.unregisteredFacts ?? []);
|
|
950
970
|
} catch (error) {
|
|
951
971
|
printError(error);
|
|
952
972
|
process.exit(1);
|
|
@@ -970,7 +990,7 @@ ${data.length} total`);
|
|
|
970
990
|
try {
|
|
971
991
|
const globalOpts = program.opts();
|
|
972
992
|
const body = JSON.parse(opts.json);
|
|
973
|
-
const data = await
|
|
993
|
+
const { data, meta } = await apiRequestWithMeta(
|
|
974
994
|
"PUT",
|
|
975
995
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/rules/${opts.id}`,
|
|
976
996
|
{
|
|
@@ -982,6 +1002,7 @@ ${data.length} total`);
|
|
|
982
1002
|
}
|
|
983
1003
|
);
|
|
984
1004
|
printJson(data);
|
|
1005
|
+
printUnregisteredFactsWarning(meta?.unregisteredFacts ?? []);
|
|
985
1006
|
} catch (error) {
|
|
986
1007
|
printError(error);
|
|
987
1008
|
process.exit(1);
|
|
@@ -996,7 +1017,7 @@ ${data.length} total`);
|
|
|
996
1017
|
const answer = await rl.question(`Delete rule ${opts.id}? [y/N] `);
|
|
997
1018
|
rl.close();
|
|
998
1019
|
if (answer.toLowerCase() !== "y") {
|
|
999
|
-
console.log("
|
|
1020
|
+
console.log("Canceled.");
|
|
1000
1021
|
return;
|
|
1001
1022
|
}
|
|
1002
1023
|
}
|
|
@@ -1099,6 +1120,7 @@ function registerFactCommands(program) {
|
|
|
1099
1120
|
|
|
1100
1121
|
Commands:
|
|
1101
1122
|
list List all fact definitions
|
|
1123
|
+
unregistered List facts referenced by a version but not yet defined
|
|
1102
1124
|
create Register a new fact
|
|
1103
1125
|
update Update fact metadata
|
|
1104
1126
|
delete Remove a fact definition
|
|
@@ -1143,6 +1165,52 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1143
1165
|
process.exit(1);
|
|
1144
1166
|
}
|
|
1145
1167
|
});
|
|
1168
|
+
facts.command("unregistered").description("List facts referenced by a version but not yet defined").requiredOption("--group-id <groupId>", "Policy group ID").requiredOption("--version-id <versionId>", "Policy version ID").addHelpText(
|
|
1169
|
+
"after",
|
|
1170
|
+
dedent6`
|
|
1171
|
+
|
|
1172
|
+
Facts used in a version's rules that have no definition yet (read-only).
|
|
1173
|
+
Does not block publish/deploy — register them to enable validation.
|
|
1174
|
+
|
|
1175
|
+
Example:
|
|
1176
|
+
$ lexq facts unregistered --group-id <gid> --version-id <vid> --format table
|
|
1177
|
+
`
|
|
1178
|
+
).action(async (opts) => {
|
|
1179
|
+
try {
|
|
1180
|
+
const globalOpts = program.opts();
|
|
1181
|
+
const format = globalOpts.format ?? "json";
|
|
1182
|
+
const data = await apiRequest(
|
|
1183
|
+
"GET",
|
|
1184
|
+
`policy-groups/${opts.groupId}/versions/${opts.versionId}/unregistered-facts`,
|
|
1185
|
+
{
|
|
1186
|
+
apiKey: globalOpts.apiKey,
|
|
1187
|
+
baseUrl: globalOpts.baseUrl,
|
|
1188
|
+
dryRun: globalOpts.dryRun,
|
|
1189
|
+
verbose: globalOpts.verbose
|
|
1190
|
+
}
|
|
1191
|
+
);
|
|
1192
|
+
if (format === "table") {
|
|
1193
|
+
printTable(
|
|
1194
|
+
["Key", "Type", "Suggested Name", "Conflict", "Sources"],
|
|
1195
|
+
data.map((f) => [
|
|
1196
|
+
f.key,
|
|
1197
|
+
f.inferredType ?? f.candidateTypes?.join("|") ?? "?",
|
|
1198
|
+
f.suggestedName,
|
|
1199
|
+
f.conflict ? "\u2713" : "\u2013",
|
|
1200
|
+
f.sources.map((s) => `${s.kind}:${s.field}`).join(", ")
|
|
1201
|
+
]),
|
|
1202
|
+
{ truncate: 28 }
|
|
1203
|
+
);
|
|
1204
|
+
console.log(`
|
|
1205
|
+
${data.length} unregistered`);
|
|
1206
|
+
} else {
|
|
1207
|
+
printJson(data);
|
|
1208
|
+
}
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
printError(error);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
});
|
|
1146
1214
|
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(
|
|
1147
1215
|
"after",
|
|
1148
1216
|
dedent6`
|
|
@@ -1220,7 +1288,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1220
1288
|
const answer = await rl.question(`Delete fact ${opts.id}? [y/N] `);
|
|
1221
1289
|
rl.close();
|
|
1222
1290
|
if (answer.toLowerCase() !== "y") {
|
|
1223
|
-
console.log("
|
|
1291
|
+
console.log("Canceled.");
|
|
1224
1292
|
return;
|
|
1225
1293
|
}
|
|
1226
1294
|
}
|
|
@@ -1316,6 +1384,7 @@ function registerDeployCommands(program) {
|
|
|
1316
1384
|
).action(async (opts) => {
|
|
1317
1385
|
try {
|
|
1318
1386
|
const globalOpts = program.opts();
|
|
1387
|
+
await warnUnregisteredFacts(globalOpts, opts.groupId, opts.versionId);
|
|
1319
1388
|
await apiRequest(
|
|
1320
1389
|
"POST",
|
|
1321
1390
|
`policy-groups/${opts.groupId}/versions/${opts.versionId}/publish`,
|
|
@@ -1345,6 +1414,7 @@ function registerDeployCommands(program) {
|
|
|
1345
1414
|
).action(async (opts) => {
|
|
1346
1415
|
try {
|
|
1347
1416
|
const globalOpts = program.opts();
|
|
1417
|
+
await warnUnregisteredFacts(globalOpts, opts.groupId, opts.versionId);
|
|
1348
1418
|
await apiRequest("POST", `policy-groups/${opts.groupId}/deploy`, {
|
|
1349
1419
|
apiKey: globalOpts.apiKey,
|
|
1350
1420
|
baseUrl: globalOpts.baseUrl,
|
|
@@ -1377,7 +1447,7 @@ function registerDeployCommands(program) {
|
|
|
1377
1447
|
const answer = await rl.question(`Rollback group ${opts.groupId}? [y/N] `);
|
|
1378
1448
|
rl.close();
|
|
1379
1449
|
if (answer.toLowerCase() !== "y") {
|
|
1380
|
-
console.log("
|
|
1450
|
+
console.log("Canceled.");
|
|
1381
1451
|
return;
|
|
1382
1452
|
}
|
|
1383
1453
|
}
|
|
@@ -1412,7 +1482,7 @@ function registerDeployCommands(program) {
|
|
|
1412
1482
|
const answer = await rl.question(`Undeploy group ${opts.groupId}? [y/N] `);
|
|
1413
1483
|
rl.close();
|
|
1414
1484
|
if (answer.toLowerCase() !== "y") {
|
|
1415
|
-
console.log("
|
|
1485
|
+
console.log("Canceled.");
|
|
1416
1486
|
return;
|
|
1417
1487
|
}
|
|
1418
1488
|
}
|
|
@@ -1588,6 +1658,18 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1588
1658
|
}
|
|
1589
1659
|
});
|
|
1590
1660
|
}
|
|
1661
|
+
async function warnUnregisteredFacts(globalOpts, groupId, versionId) {
|
|
1662
|
+
if (globalOpts.dryRun) return;
|
|
1663
|
+
try {
|
|
1664
|
+
const facts = await apiRequest(
|
|
1665
|
+
"GET",
|
|
1666
|
+
`policy-groups/${groupId}/versions/${versionId}/unregistered-facts`,
|
|
1667
|
+
{ apiKey: globalOpts.apiKey, baseUrl: globalOpts.baseUrl, verbose: globalOpts.verbose }
|
|
1668
|
+
);
|
|
1669
|
+
printUnregisteredFactsWarning(facts);
|
|
1670
|
+
} catch {
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1591
1673
|
|
|
1592
1674
|
// src/commands/analytics.ts
|
|
1593
1675
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -1867,10 +1949,7 @@ function registerAnalyticsCommands(program) {
|
|
|
1867
1949
|
process.exit(1);
|
|
1868
1950
|
}
|
|
1869
1951
|
});
|
|
1870
|
-
sim.command("list").description("List simulation history").option(
|
|
1871
|
-
"--status <status>",
|
|
1872
|
-
"Filter by status (PENDING, RUNNING, COMPLETED, FAILED, CANCELLED)"
|
|
1873
|
-
).option("--from <date>", "Start date (yyyy-MM-dd)").option("--to <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
|
|
1952
|
+
sim.command("list").description("List simulation history").option("--status <status>", "Filter by status (PENDING, RUNNING, COMPLETED, FAILED, CANCELED)").option("--from <date>", "Start date (yyyy-MM-dd)").option("--to <date>", "End date (yyyy-MM-dd)").option("--page <number>", "Page number", "0").option("--size <number>", "Page size", "20").action(async (opts) => {
|
|
1874
1953
|
try {
|
|
1875
1954
|
const globalOpts = program.opts();
|
|
1876
1955
|
const format = globalOpts.format ?? "json";
|
|
@@ -1917,7 +1996,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1917
1996
|
"after",
|
|
1918
1997
|
dedent8`
|
|
1919
1998
|
|
|
1920
|
-
Only PENDING or RUNNING simulations can be
|
|
1999
|
+
Only PENDING or RUNNING simulations can be canceled.
|
|
1921
2000
|
`
|
|
1922
2001
|
).action(async (opts) => {
|
|
1923
2002
|
try {
|
|
@@ -1928,7 +2007,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1928
2007
|
const answer = await rl.question(`Cancel simulation ${opts.id}? [y/N] `);
|
|
1929
2008
|
rl.close();
|
|
1930
2009
|
if (answer.toLowerCase() !== "y") {
|
|
1931
|
-
console.log("
|
|
2010
|
+
console.log("Canceled.");
|
|
1932
2011
|
return;
|
|
1933
2012
|
}
|
|
1934
2013
|
}
|
|
@@ -1938,7 +2017,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
1938
2017
|
dryRun: globalOpts.dryRun,
|
|
1939
2018
|
verbose: globalOpts.verbose
|
|
1940
2019
|
});
|
|
1941
|
-
console.log(`\u2713 Simulation ${opts.id}
|
|
2020
|
+
console.log(`\u2713 Simulation ${opts.id} canceled.`);
|
|
1942
2021
|
} catch (error) {
|
|
1943
2022
|
printError(error);
|
|
1944
2023
|
process.exit(1);
|
|
@@ -2388,7 +2467,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2388
2467
|
const answer = await rl.question(`Delete integration ${opts.id}? [y/N] `);
|
|
2389
2468
|
rl.close();
|
|
2390
2469
|
if (answer.toLowerCase() !== "y") {
|
|
2391
|
-
console.log("
|
|
2470
|
+
console.log("Canceled.");
|
|
2392
2471
|
return;
|
|
2393
2472
|
}
|
|
2394
2473
|
}
|
|
@@ -2765,7 +2844,7 @@ ${data.totalElements} total \xB7 page ${data.pageNo + 1}/${data.totalPages}`);
|
|
|
2765
2844
|
const answer = await rl.question(`Delete webhook subscription ${opts.id}? [y/N] `);
|
|
2766
2845
|
rl.close();
|
|
2767
2846
|
if (answer.toLowerCase() !== "y") {
|
|
2768
|
-
console.log("
|
|
2847
|
+
console.log("Canceled.");
|
|
2769
2848
|
return;
|
|
2770
2849
|
}
|
|
2771
2850
|
}
|
|
@@ -2861,14 +2940,17 @@ function createCallApiFromConfig() {
|
|
|
2861
2940
|
apiKey: config.apiKey,
|
|
2862
2941
|
baseUrl: config.baseUrl
|
|
2863
2942
|
};
|
|
2864
|
-
const data = await
|
|
2943
|
+
const { data, meta } = await apiRequestWithMeta(method, path, {
|
|
2865
2944
|
...clientOpts,
|
|
2866
2945
|
body: opts?.body,
|
|
2867
2946
|
params: opts?.params
|
|
2868
2947
|
});
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2948
|
+
const content = [
|
|
2949
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
2950
|
+
];
|
|
2951
|
+
const warning = formatUnregisteredFactWarning(meta);
|
|
2952
|
+
if (warning) content.push({ type: "text", text: warning });
|
|
2953
|
+
return { content };
|
|
2872
2954
|
} catch (error) {
|
|
2873
2955
|
const message = error instanceof Error ? error.message : String(error);
|
|
2874
2956
|
return {
|
|
@@ -2884,6 +2966,22 @@ function paginationParams(page, size) {
|
|
|
2884
2966
|
if (size !== void 0) params.size = String(size);
|
|
2885
2967
|
return params;
|
|
2886
2968
|
}
|
|
2969
|
+
function formatUnregisteredFactWarning(meta) {
|
|
2970
|
+
const facts = meta?.unregisteredFacts ?? [];
|
|
2971
|
+
if (facts.length === 0) return null;
|
|
2972
|
+
const lines = facts.map((f) => {
|
|
2973
|
+
if (f.inferredType) {
|
|
2974
|
+
return ` \u2022 ${f.key} (${f.inferredType}) \u2014 register: lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: "${f.inferredType}" })`;
|
|
2975
|
+
}
|
|
2976
|
+
const choices = f.candidateTypes?.join(" | ") ?? "STRING | NUMBER | BOOLEAN";
|
|
2977
|
+
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
2978
|
+
});
|
|
2979
|
+
return [
|
|
2980
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved \u2014 this does not block \u2014 but undefined facts skip type validation and the dry-run requirements analyzer.`,
|
|
2981
|
+
...lines,
|
|
2982
|
+
`Register them now so the rule validates as intended, or call lexq_facts_unregistered to review the version's full list.`
|
|
2983
|
+
].join("\n");
|
|
2984
|
+
}
|
|
2887
2985
|
|
|
2888
2986
|
// src/mcp/tools/status.ts
|
|
2889
2987
|
function registerStatusTools(server, callApi) {
|
|
@@ -3160,6 +3258,9 @@ function registerRuleTools(server, callApi) {
|
|
|
3160
3258
|
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
3161
3259
|
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
3162
3260
|
autocomplete, and the dry-run requirements analyzer.
|
|
3261
|
+
|
|
3262
|
+
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
3263
|
+
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
3163
3264
|
|
|
3164
3265
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
3165
3266
|
Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
|
|
@@ -3345,6 +3446,18 @@ function registerFactTools(server, callApi) {
|
|
|
3345
3446
|
},
|
|
3346
3447
|
async () => callApi("GET", "schema/action-metadata")
|
|
3347
3448
|
);
|
|
3449
|
+
server.registerTool(
|
|
3450
|
+
"lexq_facts_unregistered",
|
|
3451
|
+
{
|
|
3452
|
+
title: "List Unregistered Facts",
|
|
3453
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
3454
|
+
inputSchema: {
|
|
3455
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
3456
|
+
versionId: z4.string().uuid().describe("Version ID")
|
|
3457
|
+
}
|
|
3458
|
+
},
|
|
3459
|
+
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
3460
|
+
);
|
|
3348
3461
|
}
|
|
3349
3462
|
|
|
3350
3463
|
// src/mcp/tools/deploy.ts
|
|
@@ -3354,7 +3467,7 @@ function registerDeployTools(server, callApi) {
|
|
|
3354
3467
|
"lexq_deploy_publish",
|
|
3355
3468
|
{
|
|
3356
3469
|
title: "Publish Version",
|
|
3357
|
-
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule.",
|
|
3470
|
+
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
|
|
3358
3471
|
inputSchema: {
|
|
3359
3472
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
3360
3473
|
versionId: z5.string().uuid().describe("Version ID to publish"),
|
|
@@ -3367,7 +3480,7 @@ function registerDeployTools(server, callApi) {
|
|
|
3367
3480
|
"lexq_deploy_live",
|
|
3368
3481
|
{
|
|
3369
3482
|
title: "Deploy to Live",
|
|
3370
|
-
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately.",
|
|
3483
|
+
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
|
|
3371
3484
|
inputSchema: {
|
|
3372
3485
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
3373
3486
|
versionId: z5.string().uuid().describe("Version ID to deploy"),
|
|
@@ -3597,7 +3710,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
3597
3710
|
inputSchema: {
|
|
3598
3711
|
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
3599
3712
|
size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
3600
|
-
status: z6.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "
|
|
3713
|
+
status: z6.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
|
|
3601
3714
|
from: z6.string().optional().describe("Start date (yyyy-MM-dd)"),
|
|
3602
3715
|
to: z6.string().optional().describe("End date (yyyy-MM-dd)")
|
|
3603
3716
|
}
|
|
@@ -4175,7 +4288,7 @@ function registerDomainTemplateCommands(program) {
|
|
|
4175
4288
|
);
|
|
4176
4289
|
rl.close();
|
|
4177
4290
|
if (answer.toLowerCase() !== "y") {
|
|
4178
|
-
console.log("
|
|
4291
|
+
console.log("Canceled.");
|
|
4179
4292
|
return;
|
|
4180
4293
|
}
|
|
4181
4294
|
}
|
package/dist/mcp/register.d.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
|
|
3
|
+
declare const ValueType: readonly ["STRING", "NUMBER", "BOOLEAN", "LIST_STRING", "LIST_NUMBER"];
|
|
4
|
+
type ValueType = (typeof ValueType)[number];
|
|
5
|
+
declare const Confidence: readonly ["EXACT", "AMBIGUOUS"];
|
|
6
|
+
type Confidence = (typeof Confidence)[number];
|
|
7
|
+
declare const SourceKind: readonly ["CONDITION", "ACTION"];
|
|
8
|
+
type SourceKind = (typeof SourceKind)[number];
|
|
9
|
+
|
|
10
|
+
interface ResponseMeta {
|
|
11
|
+
unregisteredFacts?: UnregisteredFact[];
|
|
12
|
+
}
|
|
13
|
+
interface UnregisteredFact {
|
|
14
|
+
key: string;
|
|
15
|
+
inferredType: ValueType | null;
|
|
16
|
+
confidence: Confidence;
|
|
17
|
+
conflict: boolean;
|
|
18
|
+
candidateTypes: ValueType[] | null;
|
|
19
|
+
suggestedName: string;
|
|
20
|
+
sources: UnregisteredFactSource[];
|
|
21
|
+
}
|
|
22
|
+
interface UnregisteredFactSource {
|
|
23
|
+
kind: SourceKind;
|
|
24
|
+
field: string;
|
|
25
|
+
operator?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
interface McpToolResult {
|
|
4
29
|
[key: string]: unknown;
|
|
5
30
|
content: Array<{
|
|
@@ -25,13 +50,22 @@ type CallApi = (method: string, path: string, opts?: {
|
|
|
25
50
|
};
|
|
26
51
|
}) => Promise<McpToolResult>;
|
|
27
52
|
declare function paginationParams(page?: number, size?: number): Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Builds a salient, actionable warning from a response's meta when it reports facts that
|
|
55
|
+
* rules reference but the schema does not define. Returns null when there is nothing to warn.
|
|
56
|
+
*
|
|
57
|
+
* Shared by both CallApi implementations (CLI stdio here, lexq-mcp HTTP via @lexq/cli/mcp)
|
|
58
|
+
* so the agent-facing text is identical on every surface. The backend only populates
|
|
59
|
+
* meta.unregisteredFacts on rule create/update, so this only ever fires there.
|
|
60
|
+
*/
|
|
61
|
+
declare function formatUnregisteredFactWarning(meta: ResponseMeta | null | undefined): string | null;
|
|
28
62
|
|
|
29
63
|
/**
|
|
30
|
-
* Registers all
|
|
64
|
+
* Registers all MCP tools on the given server.
|
|
31
65
|
*
|
|
32
66
|
* @param server - McpServer instance
|
|
33
67
|
* @param callApi - API caller function (config-based for CLI, Bearer-based for HTTP)
|
|
34
68
|
*/
|
|
35
69
|
declare function registerAllTools(server: McpServer, callApi: CallApi): void;
|
|
36
70
|
|
|
37
|
-
export { type CallApi, type McpToolResult, paginationParams, registerAllTools };
|
|
71
|
+
export { type CallApi, type McpToolResult, formatUnregisteredFactWarning, paginationParams, registerAllTools };
|
package/dist/mcp/register.js
CHANGED
|
@@ -161,6 +161,22 @@ function paginationParams(page, size) {
|
|
|
161
161
|
if (size !== void 0) params.size = String(size);
|
|
162
162
|
return params;
|
|
163
163
|
}
|
|
164
|
+
function formatUnregisteredFactWarning(meta) {
|
|
165
|
+
const facts = meta?.unregisteredFacts ?? [];
|
|
166
|
+
if (facts.length === 0) return null;
|
|
167
|
+
const lines = facts.map((f) => {
|
|
168
|
+
if (f.inferredType) {
|
|
169
|
+
return ` \u2022 ${f.key} (${f.inferredType}) \u2014 register: lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: "${f.inferredType}" })`;
|
|
170
|
+
}
|
|
171
|
+
const choices = f.candidateTypes?.join(" | ") ?? "STRING | NUMBER | BOOLEAN";
|
|
172
|
+
return ` \u2022 ${f.key} (ambiguous) \u2014 pick a type (${choices}), then lexq_facts_create({ key: "${f.key}", name: "${f.suggestedName}", type: <chosen> })`;
|
|
173
|
+
});
|
|
174
|
+
return [
|
|
175
|
+
`\u26A0 The saved rule references ${facts.length} fact(s) not defined in the schema. The rule was saved \u2014 this does not block \u2014 but undefined facts skip type validation and the dry-run requirements analyzer.`,
|
|
176
|
+
...lines,
|
|
177
|
+
`Register them now so the rule validates as intended, or call lexq_facts_unregistered to review the version's full list.`
|
|
178
|
+
].join("\n");
|
|
179
|
+
}
|
|
164
180
|
|
|
165
181
|
// src/mcp/tools/versions.ts
|
|
166
182
|
function registerVersionTools(server, callApi) {
|
|
@@ -290,6 +306,9 @@ function registerRuleTools(server, callApi) {
|
|
|
290
306
|
If a required key is missing, ask the user to confirm the type, isRequired, and description
|
|
291
307
|
before calling lexq_facts_create — registering facts enables type validation, Console UI
|
|
292
308
|
autocomplete, and the dry-run requirements analyzer.
|
|
309
|
+
|
|
310
|
+
After saving, lexq_facts_unregistered lists any keys this version references but has not
|
|
311
|
+
defined (non-blocking, version-wide) — use it to decide what to register.
|
|
293
312
|
|
|
294
313
|
Condition: { type: "SINGLE", field, operator, value, valueType } or { type: "GROUP", operator: "AND"|"OR", children: [...] }
|
|
295
314
|
Operators: EQUALS, NOT_EQUALS, GREATER_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN, LESS_THAN_OR_EQUAL, CONTAINS, IN, NOT_IN
|
|
@@ -475,6 +494,18 @@ function registerFactTools(server, callApi) {
|
|
|
475
494
|
},
|
|
476
495
|
async () => callApi("GET", "schema/action-metadata")
|
|
477
496
|
);
|
|
497
|
+
server.registerTool(
|
|
498
|
+
"lexq_facts_unregistered",
|
|
499
|
+
{
|
|
500
|
+
title: "List Unregistered Facts",
|
|
501
|
+
description: "List facts referenced by a version's rules but not yet defined (read-only \u2014 does not block publish/deploy, INV-4). Version-wide: covers every rule in the version. Each entry carries the inferred type, suggested name, and where it is referenced (condition/action). Register them with lexq_facts_create to enable type validation and the dry-run requirements analyzer.",
|
|
502
|
+
inputSchema: {
|
|
503
|
+
groupId: z4.string().uuid().describe("Policy group ID"),
|
|
504
|
+
versionId: z4.string().uuid().describe("Version ID")
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
async ({ groupId, versionId }) => callApi("GET", `policy-groups/${groupId}/versions/${versionId}/unregistered-facts`)
|
|
508
|
+
);
|
|
478
509
|
}
|
|
479
510
|
|
|
480
511
|
// src/mcp/tools/deploy.ts
|
|
@@ -484,7 +515,7 @@ function registerDeployTools(server, callApi) {
|
|
|
484
515
|
"lexq_deploy_publish",
|
|
485
516
|
{
|
|
486
517
|
title: "Publish Version",
|
|
487
|
-
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule.",
|
|
518
|
+
description: "Publish a DRAFT version (DRAFT \u2192 ACTIVE). Locks the version from further edits. Must have at least one rule. Undefined facts referenced by rules do not block publishing (INV-4); call lexq_facts_unregistered first to review them.",
|
|
488
519
|
inputSchema: {
|
|
489
520
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
490
521
|
versionId: z5.string().uuid().describe("Version ID to publish"),
|
|
@@ -497,7 +528,7 @@ function registerDeployTools(server, callApi) {
|
|
|
497
528
|
"lexq_deploy_live",
|
|
498
529
|
{
|
|
499
530
|
title: "Deploy to Live",
|
|
500
|
-
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately.",
|
|
531
|
+
description: "Deploy an ACTIVE (published) version to live traffic. Takes effect immediately. Undefined facts do not block deployment (INV-4); use lexq_facts_unregistered to review what the version references but has not defined.",
|
|
501
532
|
inputSchema: {
|
|
502
533
|
groupId: z5.string().uuid().describe("Policy group ID"),
|
|
503
534
|
versionId: z5.string().uuid().describe("Version ID to deploy"),
|
|
@@ -727,7 +758,7 @@ function registerAnalyticsTools(server, callApi) {
|
|
|
727
758
|
inputSchema: {
|
|
728
759
|
page: z6.number().int().min(0).default(0).describe("Page number"),
|
|
729
760
|
size: z6.number().int().min(1).max(100).default(20).describe("Page size"),
|
|
730
|
-
status: z6.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "
|
|
761
|
+
status: z6.enum(["PENDING", "RUNNING", "COMPLETED", "FAILED", "CANCELED"]).optional().describe("Filter by status"),
|
|
731
762
|
from: z6.string().optional().describe("Start date (yyyy-MM-dd)"),
|
|
732
763
|
to: z6.string().optional().describe("End date (yyyy-MM-dd)")
|
|
733
764
|
}
|
|
@@ -1175,6 +1206,7 @@ function registerAllTools(server, callApi) {
|
|
|
1175
1206
|
registerWebhookSubscriptionTools(server, callApi);
|
|
1176
1207
|
}
|
|
1177
1208
|
export {
|
|
1209
|
+
formatUnregisteredFactWarning,
|
|
1178
1210
|
paginationParams,
|
|
1179
1211
|
registerAllTools
|
|
1180
1212
|
};
|
package/package.json
CHANGED
|
@@ -260,7 +260,7 @@ Simulation is async. Poll until `status` is `COMPLETED` or `FAILED`.
|
|
|
260
260
|
| `RUNNING` | In progress (`progress` field shows 0–100) |
|
|
261
261
|
| `COMPLETED` | Done — results available |
|
|
262
262
|
| `FAILED` | Error occurred |
|
|
263
|
-
| `
|
|
263
|
+
| `CANCELED` | Manually canceled |
|
|
264
264
|
|
|
265
265
|
### List Simulations
|
|
266
266
|
|