@erdoai/cli 0.55.1 → 0.56.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 +142 -11
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -362,11 +362,17 @@ var ErdoClient = class {
362
362
  `/v1/evals/suites/${encodeURIComponent(slug)}/cases/${encodeURIComponent(caseName)}`
363
363
  );
364
364
  }
365
- runEvalSuite(slug, concurrency) {
365
+ runEvalSuite(slug, concurrency, commitSha, frontendCommitSha, backendCommitSha, source) {
366
366
  return this.request(
367
367
  "POST",
368
368
  `/v1/evals/suites/${encodeURIComponent(slug)}/run`,
369
- { concurrency: concurrency ?? 0 }
369
+ {
370
+ concurrency: concurrency ?? 0,
371
+ commit_sha: commitSha,
372
+ frontend_commit_sha: frontendCommitSha,
373
+ backend_commit_sha: backendCommitSha,
374
+ source
375
+ }
370
376
  );
371
377
  }
372
378
  getEvalRun(runID) {
@@ -375,6 +381,14 @@ var ErdoClient = class {
375
381
  `/v1/evals/runs/${encodeURIComponent(runID)}`
376
382
  );
377
383
  }
384
+ getEvalWidgetCanary() {
385
+ return this.request("GET", "/v1/evals/widget-canary");
386
+ }
387
+ configureEvalWidgetCanary(widget) {
388
+ return this.request("PUT", "/v1/evals/widget-canary", {
389
+ widget
390
+ });
391
+ }
378
392
  // --- projects ---
379
393
  listProjects(params) {
380
394
  const q = new URLSearchParams();
@@ -866,6 +880,11 @@ var ErdoClient = class {
866
880
  setKnowledgeVisibility(id, visibility) {
867
881
  return this.request("PATCH", `/v1/knowledge/${id}/visibility`, { visibility });
868
882
  }
883
+ // asset_id is what a page's `assetId` field takes — attaching an image here
884
+ // is how it becomes something a published page can render.
885
+ attachKnowledgeAsset(objectID, input) {
886
+ return this.request("POST", `/v1/knowledge/${encodeURIComponent(objectID)}/attachments`, input);
887
+ }
869
888
  // --- automations (heartbeats) ---
870
889
  listHeartbeats() {
871
890
  return this.request("GET", "/v1/heartbeats");
@@ -1384,6 +1403,20 @@ function summariseResults(results, cases = []) {
1384
1403
  }
1385
1404
  }
1386
1405
  }
1406
+ function printEvalRunAttribution(run) {
1407
+ console.log(`source: ${run.triggered_by}`);
1408
+ if (run.commit_sha) console.log(`commit: ${run.commit_sha}`);
1409
+ if (run.frontend_commit_sha) console.log(`frontend commit: ${run.frontend_commit_sha}`);
1410
+ if (run.backend_commit_sha) console.log(`backend commit: ${run.backend_commit_sha}`);
1411
+ }
1412
+ function compactEvalRunAttribution(run) {
1413
+ const revisions = [
1414
+ run.commit_sha ? `commit ${run.commit_sha.slice(0, 12)}` : void 0,
1415
+ run.frontend_commit_sha ? `frontend ${run.frontend_commit_sha.slice(0, 12)}` : void 0,
1416
+ run.backend_commit_sha ? `backend ${run.backend_commit_sha.slice(0, 12)}` : void 0
1417
+ ].filter((part) => part !== void 0);
1418
+ return [run.triggered_by, ...revisions].join(" ");
1419
+ }
1387
1420
  var program = new Command();
1388
1421
  program.name("erdo").description("Erdo CLI").version(pkg.version).option("--org <idOrSlug>", "org to target for this command (overrides the active org)").option("--project <uuid>", "project context for this command (must belong to the active org)");
1389
1422
  program.hook("preAction", (thisCommand) => {
@@ -2228,18 +2261,41 @@ function caseScoringPayload(opts) {
2228
2261
  }
2229
2262
  return out;
2230
2263
  }
2231
- evalCmd.command("update <slug>").description("Update a suite's settings (only the flags you pass change)").option("--name <name>", "new name").option("--description <text>", "new description").option("--agent <key>", "repoint the agent under test, e.g. erdo.data-question-answerer").option("--judge <model>", "new judge model").option("--threshold <n>", "new pass threshold 0-5", (v) => parseFloat(v)).option("--evaluate-artifact <bool>", "judge the rendered page (true/false)").option("--cron <bool>", "include in the daily cron (true/false)").action(
2264
+ function parseEvalThreshold(value) {
2265
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(value.trim())) {
2266
+ throw new Error("threshold must be greater than 0 and at most 5");
2267
+ }
2268
+ const threshold = Number(value);
2269
+ if (!Number.isFinite(threshold) || threshold <= 0 || threshold > 5) {
2270
+ throw new Error("threshold must be greater than 0 and at most 5");
2271
+ }
2272
+ return threshold;
2273
+ }
2274
+ function parseStrictBoolean(value) {
2275
+ switch (value.trim().toLowerCase()) {
2276
+ case "true":
2277
+ case "1":
2278
+ case "yes":
2279
+ return true;
2280
+ case "false":
2281
+ case "0":
2282
+ case "no":
2283
+ return false;
2284
+ default:
2285
+ throw new Error("value must be true or false");
2286
+ }
2287
+ }
2288
+ evalCmd.command("update <slug>").description("Update a suite's settings (only the flags you pass change)").option("--name <name>", "new name").option("--description <text>", "new description").option("--agent <key>", "repoint the agent under test, e.g. erdo.data-question-answerer").option("--judge <model>", "new judge model").option("--threshold <n>", "new pass threshold >0 and <=5", parseEvalThreshold).option("--evaluate-artifact <bool>", "judge the rendered page (true/false)").option("--cron <bool>", "include in the daily cron (true/false)").action(
2232
2289
  async (slug, opts) => {
2233
2290
  try {
2234
- const parseBool = (v) => v === void 0 ? void 0 : v === "true" || v === "1" || v === "yes";
2235
2291
  const res = await new ErdoClient().updateEvalSuite(slug, {
2236
2292
  name: opts.name,
2237
2293
  description: opts.description,
2238
2294
  agent_key: opts.agent,
2239
2295
  judge_model: opts.judge,
2240
2296
  pass_threshold: opts.threshold,
2241
- evaluate_artifact: parseBool(opts.evaluateArtifact),
2242
- cron_enabled: parseBool(opts.cron)
2297
+ evaluate_artifact: opts.evaluateArtifact === void 0 ? void 0 : parseStrictBoolean(opts.evaluateArtifact),
2298
+ cron_enabled: opts.cron === void 0 ? void 0 : parseStrictBoolean(opts.cron)
2243
2299
  });
2244
2300
  print(res);
2245
2301
  } catch (e) {
@@ -2247,7 +2303,7 @@ evalCmd.command("update <slug>").description("Update a suite's settings (only th
2247
2303
  }
2248
2304
  }
2249
2305
  );
2250
- evalCmd.command("create <name>").description("Create a suite (in the active org)").requiredOption("--agent <key>", "agent under test, e.g. erdo.artifact-builder").option("--description <text>", "description").option("--judge <model>", "judge model").option("--threshold <n>", "pass threshold 0-5", (v) => parseFloat(v)).option("--evaluate-artifact", "judge the rendered page (visual suite)").option("--no-cron", "exclude from the daily cron (recommended for artifact suites)").requiredOption("--case <json>", "first case: {name,input,rubric,tags?}", (v, acc) => {
2306
+ evalCmd.command("create <name>").description("Create a suite (in the active org)").requiredOption("--agent <key>", "agent under test, e.g. erdo.artifact-builder").option("--description <text>", "description").option("--judge <model>", "judge model").option("--threshold <n>", "pass threshold >0 and <=5", parseEvalThreshold).option("--evaluate-artifact", "judge the rendered page (visual suite)").option("--no-cron", "exclude from the daily cron (recommended for artifact suites)").requiredOption("--case <json>", "first case: {name,input,rubric,tags?}", (v, acc) => {
2251
2307
  acc.push(v);
2252
2308
  return acc;
2253
2309
  }, []).action(
@@ -2271,7 +2327,7 @@ evalCmd.command("create <name>").description("Create a suite (in the active org)
2271
2327
  }
2272
2328
  }
2273
2329
  );
2274
- evalCmd.command("run <slug>").description("Run a suite; --watch polls until it completes").option("-w, --watch", "poll until the run finishes and print results").option("-c, --concurrency <n>", "parallel cases", (v) => parseInt(v, 10)).action(async (slug, opts) => {
2330
+ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it completes").option("-w, --watch", "poll until the run finishes and print results").option("-c, --concurrency <n>", "parallel cases", (v) => parseInt(v, 10)).option("--commit-sha <sha>", "full 40-character Git commit SHA to associate with the run").option("--frontend-commit-sha <sha>", "frontend revision in the evaluated production snapshot").option("--backend-commit-sha <sha>", "backend revision in the evaluated production snapshot").option("--source <source>", "staff-only run source: post_deploy, nightly, or rollback").action(async (slug, opts) => {
2275
2331
  try {
2276
2332
  const api = new ErdoClient();
2277
2333
  const { suite } = await api.getEvalSuite(slug);
@@ -2287,7 +2343,14 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
2287
2343
  );
2288
2344
  process.exit(1);
2289
2345
  }
2290
- const { run_id } = await api.runEvalSuite(slug, opts.concurrency);
2346
+ const { run_id } = await api.runEvalSuite(
2347
+ slug,
2348
+ opts.concurrency,
2349
+ opts.commitSha,
2350
+ opts.frontendCommitSha,
2351
+ opts.backendCommitSha,
2352
+ opts.source
2353
+ );
2291
2354
  console.log(`run_id: ${run_id}`);
2292
2355
  if (!opts.watch) return;
2293
2356
  for (; ; ) {
@@ -2301,6 +2364,7 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
2301
2364
  `
2302
2365
  ${run.status}: ${run.passed_cases}/${run.total_cases} passed, avg ${run.avg_score.toFixed(2)}`
2303
2366
  );
2367
+ printEvalRunAttribution(run);
2304
2368
  summariseResults(results, cases);
2305
2369
  if (run.status !== "completed" || run.passed_cases < run.total_cases) {
2306
2370
  process.exitCode = 1;
@@ -2322,6 +2386,7 @@ evalCmd.command("results <runId>").description("Show a run's results").option("-
2322
2386
  console.log(
2323
2387
  `${run.status}: ${run.passed_cases}/${run.total_cases} passed, avg ${run.avg_score.toFixed(2)}`
2324
2388
  );
2389
+ printEvalRunAttribution(run);
2325
2390
  summariseResults(results, cases);
2326
2391
  } catch (e) {
2327
2392
  fail(e);
@@ -2332,9 +2397,41 @@ evalCmd.command("runs").description("List recent runs").option("-s, --suite <slu
2332
2397
  const { runs } = await new ErdoClient().listEvalRuns(opts.suite, opts.limit);
2333
2398
  for (const r of runs) {
2334
2399
  console.log(
2335
- `${r.id} ${r.status} ${r.passed_cases}/${r.total_cases} avg ${r.avg_score.toFixed(2)}`
2400
+ `${r.id} ${r.status} ${r.passed_cases}/${r.total_cases} avg ${r.avg_score.toFixed(2)} ${compactEvalRunAttribution(r)}`
2401
+ );
2402
+ }
2403
+ } catch (e) {
2404
+ fail(e);
2405
+ }
2406
+ });
2407
+ var evalCanaryCmd = evalCmd.command("canary").description("Show or configure the current organization's deployed-experience widget canary (staff only)");
2408
+ evalCanaryCmd.command("show").description("Show the widget designated as this organization's eval canary").action(async () => {
2409
+ try {
2410
+ print(await new ErdoClient().getEvalWidgetCanary());
2411
+ } catch (e) {
2412
+ fail(e);
2413
+ }
2414
+ });
2415
+ evalCanaryCmd.command("set <widget>").description("Designate a widget by unique name or public key in the pinned organization").action(async (widget) => {
2416
+ try {
2417
+ if (!process.env.ERDO_ORG) {
2418
+ throw new Error(
2419
+ "Refusing to change an eval canary without a pinned org. Re-run as: erdo --org <id|slug> eval canary set <widget-name-or-public-key>"
2420
+ );
2421
+ }
2422
+ print(await new ErdoClient().configureEvalWidgetCanary(widget));
2423
+ } catch (e) {
2424
+ fail(e);
2425
+ }
2426
+ });
2427
+ evalCanaryCmd.command("clear").description("Clear the pinned organization's eval widget canary").action(async () => {
2428
+ try {
2429
+ if (!process.env.ERDO_ORG) {
2430
+ throw new Error(
2431
+ "Refusing to clear an eval canary without a pinned org. Re-run as: erdo --org <id|slug> eval canary clear"
2336
2432
  );
2337
2433
  }
2434
+ print(await new ErdoClient().configureEvalWidgetCanary(""));
2338
2435
  } catch (e) {
2339
2436
  fail(e);
2340
2437
  }
@@ -2345,6 +2442,9 @@ caseCmd.command("add <slug>").description("Add a case to a suite").requiredOptio
2345
2442
  "a turn to run BEFORE input, same thread/agent (repeatable; e.g. 'create a voice widget for X')",
2346
2443
  collect,
2347
2444
  []
2445
+ ).option(
2446
+ "--target-type <type>",
2447
+ "closed deployed target: live_page_fleet, widget_fleet_live, widget_text_canary, widget_voice_canary, or widget_video_canary"
2348
2448
  ).option("--rubric <json>", 'LLM rubric: JSON array of {"criterion","weight"}').option("--evaluator <json>", "evaluator JSON {type,weight,...} (repeatable)", collect, []).option("--tags <csv>", "comma-separated tags").action(
2349
2449
  async (slug, opts) => {
2350
2450
  try {
@@ -2352,6 +2452,7 @@ caseCmd.command("add <slug>").description("Add a case to a suite").requiredOptio
2352
2452
  name: opts.name,
2353
2453
  input: opts.input,
2354
2454
  setup_messages: opts.setup,
2455
+ target_type: opts.targetType,
2355
2456
  ...caseScoringPayload(opts),
2356
2457
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
2357
2458
  });
@@ -2361,13 +2462,19 @@ caseCmd.command("add <slug>").description("Add a case to a suite").requiredOptio
2361
2462
  }
2362
2463
  }
2363
2464
  );
2364
- caseCmd.command("update <slug> <caseName>").description("Replace a case's brief, scoring, and tags").requiredOption("--input <brief>", "replacement brief/prompt (the evaluated turn)").option("--setup <msg>", "replacement pre-turn run before input (repeatable; omit for none)", collect, []).option("--rubric <json>", "replacement LLM rubric JSON").option("--evaluator <json>", "evaluator JSON {type,weight,...} (repeatable)", collect, []).option("--tags <csv>", "comma-separated tags").action(
2465
+ caseCmd.command("update <slug> <caseName>").description("Replace a case's brief, scoring, and tags").requiredOption("--input <brief>", "replacement brief/prompt (the evaluated turn)").option("--setup <msg>", "replacement pre-turn run before input (repeatable; omit to preserve)", collect, []).option(
2466
+ "--target-type <type>",
2467
+ "replacement deployed target: live_page_fleet, widget_fleet_live, widget_text_canary, widget_voice_canary, or widget_video_canary (omit to preserve)"
2468
+ ).option("--clear-setup", "remove all existing setup turns").option("--clear-target", "remove the deployed target and return to ordinary agent execution").option("--rubric <json>", "replacement LLM rubric JSON").option("--evaluator <json>", "evaluator JSON {type,weight,...} (repeatable)", collect, []).option("--tags <csv>", "comma-separated tags").action(
2365
2469
  async (slug, caseName, opts) => {
2366
2470
  try {
2367
2471
  const res = await new ErdoClient().updateEvalCase(slug, caseName, {
2368
2472
  name: caseName,
2369
2473
  input: opts.input,
2370
2474
  setup_messages: opts.setup,
2475
+ target_type: opts.targetType,
2476
+ clear_setup: !!opts.clearSetup,
2477
+ clear_target: !!opts.clearTarget,
2371
2478
  ...caseScoringPayload(opts),
2372
2479
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
2373
2480
  });
@@ -3929,6 +4036,30 @@ knowledgeCmd.command("visibility <id> <visibility>").description(
3929
4036
  fail(e);
3930
4037
  }
3931
4038
  });
4039
+ knowledgeCmd.command("attach <objectId> <file>").description(
4040
+ "Attach an image or other small binary to a knowledge object as an org-hosted asset, and print its asset id \u2014 the value a page's assetId field takes. Pass a local path, or --url to have the service download it (the only path for video)."
4041
+ ).option("--url <sourceUrl>", "download from this URL instead of reading a local file").option(
4042
+ "-d, --description <text>",
4043
+ "caption for the asset; for an image this is the only text search can match, so describe what is in the picture"
4044
+ ).option("--media-type <mime>", "MIME type hint, e.g. image/jpeg; detected from the filename otherwise").action(
4045
+ async (objectId, file, opts) => {
4046
+ try {
4047
+ const body = opts.url ? { filename: basename(file), source_url: opts.url } : {
4048
+ filename: basename(file),
4049
+ content_base64: readFileSync3(file).toString("base64")
4050
+ };
4051
+ print(
4052
+ await new ErdoClient().attachKnowledgeAsset(objectId, {
4053
+ ...body,
4054
+ description: opts.description,
4055
+ media_type: opts.mediaType
4056
+ })
4057
+ );
4058
+ } catch (e) {
4059
+ fail(e);
4060
+ }
4061
+ }
4062
+ );
3932
4063
  var autoCmd = program.command("automations").description("Scheduled automations (heartbeats)");
3933
4064
  autoCmd.command("list").description("List automations").action(async () => {
3934
4065
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.55.1",
3
+ "version": "0.56.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {