@erdoai/cli 0.24.0 → 0.26.0

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.
Files changed (2) hide show
  1. package/dist/index.js +126 -3
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -427,6 +427,32 @@ var ErdoClient = class {
427
427
  input
428
428
  );
429
429
  }
430
+ // --- knowledge review queue ---
431
+ listReviewItems(params) {
432
+ const q = new URLSearchParams();
433
+ if (params?.status !== void 0) q.set("status", params.status);
434
+ if (params?.type) q.set("type", params.type);
435
+ if (params?.limit) q.set("limit", String(params.limit));
436
+ if (params?.offset) q.set("offset", String(params.offset));
437
+ const qs = q.toString();
438
+ return this.request(
439
+ "GET",
440
+ `/v1/review-items${qs ? `?${qs}` : ""}`
441
+ );
442
+ }
443
+ getReviewItem(id) {
444
+ return this.request(
445
+ "GET",
446
+ `/v1/review-items/${encodeURIComponent(id)}`
447
+ );
448
+ }
449
+ decideReviewItem(id, input) {
450
+ return this.request(
451
+ "POST",
452
+ `/v1/review-items/${encodeURIComponent(id)}/decide`,
453
+ input
454
+ );
455
+ }
430
456
  // --- agents ---
431
457
  ask(input) {
432
458
  return this.request("POST", "/v1/ask", input);
@@ -1272,9 +1298,45 @@ wsCmd.command("attach <slug>").description("Link a resource (page, dataset, hear
1272
1298
  }
1273
1299
  }
1274
1300
  );
1301
+ var VARIANT_SPEC_KEYS = /* @__PURE__ */ new Set([
1302
+ "key",
1303
+ "label",
1304
+ "alloc",
1305
+ "allocation_percent",
1306
+ "control",
1307
+ "is_control",
1308
+ "page",
1309
+ "url",
1310
+ "treatment_url",
1311
+ "type",
1312
+ "treatment_resource_type",
1313
+ "resource",
1314
+ "treatment_resource_id",
1315
+ "class",
1316
+ "treatment_class"
1317
+ ]);
1318
+ var COMMA_SAFE_VARIANT_KEYS = /* @__PURE__ */ new Set(["url", "treatment_url", "label"]);
1319
+ function splitVariantSpec(spec) {
1320
+ const parts = [];
1321
+ let lastKey = "";
1322
+ for (const raw of spec.split(",")) {
1323
+ const trimmed = raw.trim();
1324
+ if (!trimmed) continue;
1325
+ const eq = trimmed.indexOf("=");
1326
+ const key = eq > 0 ? trimmed.slice(0, eq).trim() : "";
1327
+ const startsToken = trimmed === "control" || trimmed === "is_control" || key !== "" && VARIANT_SPEC_KEYS.has(key);
1328
+ if (!startsToken && parts.length > 0 && COMMA_SAFE_VARIANT_KEYS.has(lastKey)) {
1329
+ parts[parts.length - 1] += "," + raw;
1330
+ continue;
1331
+ }
1332
+ parts.push(raw);
1333
+ lastKey = startsToken && key !== "" ? key : "";
1334
+ }
1335
+ return parts;
1336
+ }
1275
1337
  function parseVariant(spec) {
1276
1338
  const v = {};
1277
- for (const raw of spec.split(",")) {
1339
+ for (const raw of splitVariantSpec(spec)) {
1278
1340
  const part = raw.trim();
1279
1341
  if (!part) continue;
1280
1342
  const eq = part.indexOf("=");
@@ -1283,7 +1345,7 @@ function parseVariant(spec) {
1283
1345
  v.is_control = true;
1284
1346
  continue;
1285
1347
  }
1286
- throw new Error(`--variant: unknown flag "${part}" (expected key=, label=, page=, alloc=, class=, or 'control')`);
1348
+ throw new Error(`--variant: unknown flag "${part}" (expected key=, label=, page=, url=, alloc=, class=, or 'control')`);
1287
1349
  }
1288
1350
  const k = part.slice(0, eq).trim();
1289
1351
  const val = part.slice(eq + 1).trim();
@@ -1307,6 +1369,10 @@ function parseVariant(spec) {
1307
1369
  v.treatment_resource_type = "page";
1308
1370
  v.treatment_resource_id = val;
1309
1371
  break;
1372
+ case "url":
1373
+ case "treatment_url":
1374
+ v.treatment_url = val;
1375
+ break;
1310
1376
  case "type":
1311
1377
  case "treatment_resource_type":
1312
1378
  v.treatment_resource_type = val;
@@ -1326,6 +1392,9 @@ function parseVariant(spec) {
1326
1392
  if (!v.key) {
1327
1393
  throw new Error('--variant requires key=, e.g. --variant "key=control,label=Current,control"');
1328
1394
  }
1395
+ if (v.treatment_url && (v.treatment_resource_type || v.treatment_resource_id)) {
1396
+ throw new Error("--variant: url= is mutually exclusive with page=/type=/resource= (a variant is either an Erdo page or an external URL)");
1397
+ }
1329
1398
  if (!v.label) v.label = v.key;
1330
1399
  return v;
1331
1400
  }
@@ -1386,7 +1455,7 @@ expCmd.command("policy <slug>").description(
1386
1455
  });
1387
1456
  expCmd.command("create").description("Create an experiment with its variants, evidence datasets and decision rule").requiredOption("--project <slug>", "project slug").requiredOption("--slug <slug>", "experiment slug (unique per org)").requiredOption("--title <title>", "title").option("--workstream <slug>", "host workstream").option("--scope <scope>", "local (default), platform_canary, platform_global").option("--hypothesis <text>", "hypothesis markdown").option("--primary-metric <metric>", "the metric the decision rule reads").option("--decision-rule <text>", "the rule that decides ship/stop from the primary metric").option("--guardrail <metric>", "a metric that must not regress (repeatable)", collect, []).option(
1388
1457
  "--variant <spec>",
1389
- 'a variant "key=b,label=3-field form,page=<artifact-id>,alloc=50,class=form_length_change,control" (repeatable). Fields: key=, label=, page=, alloc=, class=, and the bare flag control (or control=true) to mark the baseline. class= declares the structural change vs control for cross-experiment learning: headline_change | hero_media_change | cta_change | form_length_change | social_proof_add | layout_reorder | offer_change | full_page_rebuild',
1458
+ `a variant "key=b,label=3-field form,page=<artifact-id>,alloc=50,class=form_length_change,control" (repeatable). Fields: key=, label=, page= (Erdo artifact id) OR url=<https://\u2026> (an external page the persona panel visits, e.g. a client's live landing page \u2014 mutually exclusive with page=), alloc=, class=, and the bare flag control (or control=true) to mark the baseline. class= declares the structural change vs control for cross-experiment learning: headline_change | hero_media_change | cta_change | form_length_change | social_proof_add | layout_reorder | offer_change | full_page_rebuild`,
1390
1459
  collect,
1391
1460
  []
1392
1461
  ).option(
@@ -1982,6 +2051,60 @@ attnCmd.command("respond <id>").description(
1982
2051
  fail(e);
1983
2052
  }
1984
2053
  });
2054
+ var reviewsCmd = program.command("reviews").description(
2055
+ "The Knowledge Review queue \u2014 knowledge patches the critic proposes, investigations, and deduplicated failure signals awaiting a human decision"
2056
+ );
2057
+ reviewsCmd.command("list").description("List review items (decision items rank above failure signals)").option("--status <status>", "open (default) | snoozed | resolved | rejected | '' for all").option("--type <type>", "filter by type, e.g. knowledge_patch, eval_failure, failed_run").option("-n, --limit <n>", "max items", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).action(
2058
+ async (opts) => {
2059
+ try {
2060
+ print(
2061
+ await new ErdoClient().listReviewItems({
2062
+ status: opts.status,
2063
+ type: opts.type,
2064
+ limit: opts.limit,
2065
+ offset: opts.offset
2066
+ })
2067
+ );
2068
+ } catch (e) {
2069
+ fail(e);
2070
+ }
2071
+ }
2072
+ );
2073
+ reviewsCmd.command("show <id>").description("Show one review item with its full payload (the proposed patch body for a knowledge_patch)").action(async (id) => {
2074
+ try {
2075
+ print(await new ErdoClient().getReviewItem(id));
2076
+ } catch (e) {
2077
+ fail(e);
2078
+ }
2079
+ });
2080
+ reviewsCmd.command("decide <id>").description(
2081
+ "Decide a review item: --apply a knowledge patch (mutates Knowledge, then resolves), --resolve, --reject, or --snooze [minutes]"
2082
+ ).option("--apply", "apply a knowledge_patch: create/update the proposed Knowledge object, then resolve").option("--resolve", "close as handled").option("--reject", "close as declined").option("--snooze [minutes]", "hide the item for [minutes] (default 7 days)").option("--note <note>", "resolution note recorded on the item").action(
2083
+ async (id, opts) => {
2084
+ try {
2085
+ const chosen = [
2086
+ opts.apply ? "apply" : null,
2087
+ opts.resolve ? "resolve" : null,
2088
+ opts.reject ? "reject" : null,
2089
+ opts.snooze !== void 0 ? "snooze" : null
2090
+ ].filter(Boolean);
2091
+ if (chosen.length !== 1) {
2092
+ fail(new Error("provide exactly one of --apply, --resolve, --reject, or --snooze"));
2093
+ }
2094
+ const action = chosen[0];
2095
+ let minutes;
2096
+ if (action === "snooze" && typeof opts.snooze === "string") {
2097
+ minutes = parseInt(opts.snooze, 10);
2098
+ if (Number.isNaN(minutes)) {
2099
+ throw new Error("--snooze minutes must be a number");
2100
+ }
2101
+ }
2102
+ print(await new ErdoClient().decideReviewItem(id, { action, minutes, note: opts.note }));
2103
+ } catch (e) {
2104
+ fail(e);
2105
+ }
2106
+ }
2107
+ );
1985
2108
  var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
1986
2109
  function readMaybeFile(v) {
1987
2110
  if (!v) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Erdo CLI \u2014 drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {