@erdoai/cli 0.55.0 → 0.55.3

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 -13
  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();
@@ -783,6 +797,12 @@ var ErdoClient = class {
783
797
  connectIntegration(body) {
784
798
  return this.request("POST", "/v1/integrations-connect", body);
785
799
  }
800
+ // `connected` reports whether the connection exists, which is not the same as
801
+ // whether it works: a key retired at the provider leaves status 'connected'
802
+ // here, because the connector platform keeps signing calls with it and simply
803
+ // relays the provider's refusal. `provider_auth_failed_at` is when that last
804
+ // happened, and the `note` says so in words — a connection carrying one needs
805
+ // reauthorizing even though nothing else looks wrong.
786
806
  checkIntegrationConnection(app) {
787
807
  return this.request("GET", `/v1/integrations-connect/${encodeURIComponent(app)}`);
788
808
  }
@@ -1378,6 +1398,20 @@ function summariseResults(results, cases = []) {
1378
1398
  }
1379
1399
  }
1380
1400
  }
1401
+ function printEvalRunAttribution(run) {
1402
+ console.log(`source: ${run.triggered_by}`);
1403
+ if (run.commit_sha) console.log(`commit: ${run.commit_sha}`);
1404
+ if (run.frontend_commit_sha) console.log(`frontend commit: ${run.frontend_commit_sha}`);
1405
+ if (run.backend_commit_sha) console.log(`backend commit: ${run.backend_commit_sha}`);
1406
+ }
1407
+ function compactEvalRunAttribution(run) {
1408
+ const revisions = [
1409
+ run.commit_sha ? `commit ${run.commit_sha.slice(0, 12)}` : void 0,
1410
+ run.frontend_commit_sha ? `frontend ${run.frontend_commit_sha.slice(0, 12)}` : void 0,
1411
+ run.backend_commit_sha ? `backend ${run.backend_commit_sha.slice(0, 12)}` : void 0
1412
+ ].filter((part) => part !== void 0);
1413
+ return [run.triggered_by, ...revisions].join(" ");
1414
+ }
1381
1415
  var program = new Command();
1382
1416
  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)");
1383
1417
  program.hook("preAction", (thisCommand) => {
@@ -2222,18 +2256,41 @@ function caseScoringPayload(opts) {
2222
2256
  }
2223
2257
  return out;
2224
2258
  }
2225
- 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(
2259
+ function parseEvalThreshold(value) {
2260
+ if (!/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(value.trim())) {
2261
+ throw new Error("threshold must be greater than 0 and at most 5");
2262
+ }
2263
+ const threshold = Number(value);
2264
+ if (!Number.isFinite(threshold) || threshold <= 0 || threshold > 5) {
2265
+ throw new Error("threshold must be greater than 0 and at most 5");
2266
+ }
2267
+ return threshold;
2268
+ }
2269
+ function parseStrictBoolean(value) {
2270
+ switch (value.trim().toLowerCase()) {
2271
+ case "true":
2272
+ case "1":
2273
+ case "yes":
2274
+ return true;
2275
+ case "false":
2276
+ case "0":
2277
+ case "no":
2278
+ return false;
2279
+ default:
2280
+ throw new Error("value must be true or false");
2281
+ }
2282
+ }
2283
+ 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(
2226
2284
  async (slug, opts) => {
2227
2285
  try {
2228
- const parseBool = (v) => v === void 0 ? void 0 : v === "true" || v === "1" || v === "yes";
2229
2286
  const res = await new ErdoClient().updateEvalSuite(slug, {
2230
2287
  name: opts.name,
2231
2288
  description: opts.description,
2232
2289
  agent_key: opts.agent,
2233
2290
  judge_model: opts.judge,
2234
2291
  pass_threshold: opts.threshold,
2235
- evaluate_artifact: parseBool(opts.evaluateArtifact),
2236
- cron_enabled: parseBool(opts.cron)
2292
+ evaluate_artifact: opts.evaluateArtifact === void 0 ? void 0 : parseStrictBoolean(opts.evaluateArtifact),
2293
+ cron_enabled: opts.cron === void 0 ? void 0 : parseStrictBoolean(opts.cron)
2237
2294
  });
2238
2295
  print(res);
2239
2296
  } catch (e) {
@@ -2241,7 +2298,7 @@ evalCmd.command("update <slug>").description("Update a suite's settings (only th
2241
2298
  }
2242
2299
  }
2243
2300
  );
2244
- 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) => {
2301
+ 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) => {
2245
2302
  acc.push(v);
2246
2303
  return acc;
2247
2304
  }, []).action(
@@ -2265,7 +2322,7 @@ evalCmd.command("create <name>").description("Create a suite (in the active org)
2265
2322
  }
2266
2323
  }
2267
2324
  );
2268
- 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) => {
2325
+ 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) => {
2269
2326
  try {
2270
2327
  const api = new ErdoClient();
2271
2328
  const { suite } = await api.getEvalSuite(slug);
@@ -2281,7 +2338,14 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
2281
2338
  );
2282
2339
  process.exit(1);
2283
2340
  }
2284
- const { run_id } = await api.runEvalSuite(slug, opts.concurrency);
2341
+ const { run_id } = await api.runEvalSuite(
2342
+ slug,
2343
+ opts.concurrency,
2344
+ opts.commitSha,
2345
+ opts.frontendCommitSha,
2346
+ opts.backendCommitSha,
2347
+ opts.source
2348
+ );
2285
2349
  console.log(`run_id: ${run_id}`);
2286
2350
  if (!opts.watch) return;
2287
2351
  for (; ; ) {
@@ -2295,6 +2359,7 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
2295
2359
  `
2296
2360
  ${run.status}: ${run.passed_cases}/${run.total_cases} passed, avg ${run.avg_score.toFixed(2)}`
2297
2361
  );
2362
+ printEvalRunAttribution(run);
2298
2363
  summariseResults(results, cases);
2299
2364
  if (run.status !== "completed" || run.passed_cases < run.total_cases) {
2300
2365
  process.exitCode = 1;
@@ -2316,6 +2381,7 @@ evalCmd.command("results <runId>").description("Show a run's results").option("-
2316
2381
  console.log(
2317
2382
  `${run.status}: ${run.passed_cases}/${run.total_cases} passed, avg ${run.avg_score.toFixed(2)}`
2318
2383
  );
2384
+ printEvalRunAttribution(run);
2319
2385
  summariseResults(results, cases);
2320
2386
  } catch (e) {
2321
2387
  fail(e);
@@ -2326,19 +2392,54 @@ evalCmd.command("runs").description("List recent runs").option("-s, --suite <slu
2326
2392
  const { runs } = await new ErdoClient().listEvalRuns(opts.suite, opts.limit);
2327
2393
  for (const r of runs) {
2328
2394
  console.log(
2329
- `${r.id} ${r.status} ${r.passed_cases}/${r.total_cases} avg ${r.avg_score.toFixed(2)}`
2395
+ `${r.id} ${r.status} ${r.passed_cases}/${r.total_cases} avg ${r.avg_score.toFixed(2)} ${compactEvalRunAttribution(r)}`
2330
2396
  );
2331
2397
  }
2332
2398
  } catch (e) {
2333
2399
  fail(e);
2334
2400
  }
2335
2401
  });
2402
+ var evalCanaryCmd = evalCmd.command("canary").description("Show or configure the current organization's deployed-experience widget canary (staff only)");
2403
+ evalCanaryCmd.command("show").description("Show the widget designated as this organization's eval canary").action(async () => {
2404
+ try {
2405
+ print(await new ErdoClient().getEvalWidgetCanary());
2406
+ } catch (e) {
2407
+ fail(e);
2408
+ }
2409
+ });
2410
+ evalCanaryCmd.command("set <widget>").description("Designate a widget by unique name or public key in the pinned organization").action(async (widget) => {
2411
+ try {
2412
+ if (!process.env.ERDO_ORG) {
2413
+ throw new Error(
2414
+ "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>"
2415
+ );
2416
+ }
2417
+ print(await new ErdoClient().configureEvalWidgetCanary(widget));
2418
+ } catch (e) {
2419
+ fail(e);
2420
+ }
2421
+ });
2422
+ evalCanaryCmd.command("clear").description("Clear the pinned organization's eval widget canary").action(async () => {
2423
+ try {
2424
+ if (!process.env.ERDO_ORG) {
2425
+ throw new Error(
2426
+ "Refusing to clear an eval canary without a pinned org. Re-run as: erdo --org <id|slug> eval canary clear"
2427
+ );
2428
+ }
2429
+ print(await new ErdoClient().configureEvalWidgetCanary(""));
2430
+ } catch (e) {
2431
+ fail(e);
2432
+ }
2433
+ });
2336
2434
  var caseCmd = evalCmd.command("case").description("Manage cases in a suite");
2337
2435
  caseCmd.command("add <slug>").description("Add a case to a suite").requiredOption("--name <name>", "case name").requiredOption("--input <brief>", "the brief/prompt sent to the agent (the evaluated turn)").option(
2338
2436
  "--setup <msg>",
2339
2437
  "a turn to run BEFORE input, same thread/agent (repeatable; e.g. 'create a voice widget for X')",
2340
2438
  collect,
2341
2439
  []
2440
+ ).option(
2441
+ "--target-type <type>",
2442
+ "closed deployed target: live_page_fleet, widget_fleet_live, widget_text_canary, widget_voice_canary, or widget_video_canary"
2342
2443
  ).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(
2343
2444
  async (slug, opts) => {
2344
2445
  try {
@@ -2346,6 +2447,7 @@ caseCmd.command("add <slug>").description("Add a case to a suite").requiredOptio
2346
2447
  name: opts.name,
2347
2448
  input: opts.input,
2348
2449
  setup_messages: opts.setup,
2450
+ target_type: opts.targetType,
2349
2451
  ...caseScoringPayload(opts),
2350
2452
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
2351
2453
  });
@@ -2355,13 +2457,19 @@ caseCmd.command("add <slug>").description("Add a case to a suite").requiredOptio
2355
2457
  }
2356
2458
  }
2357
2459
  );
2358
- 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(
2460
+ 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(
2461
+ "--target-type <type>",
2462
+ "replacement deployed target: live_page_fleet, widget_fleet_live, widget_text_canary, widget_voice_canary, or widget_video_canary (omit to preserve)"
2463
+ ).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(
2359
2464
  async (slug, caseName, opts) => {
2360
2465
  try {
2361
2466
  const res = await new ErdoClient().updateEvalCase(slug, caseName, {
2362
2467
  name: caseName,
2363
2468
  input: opts.input,
2364
2469
  setup_messages: opts.setup,
2470
+ target_type: opts.targetType,
2471
+ clear_setup: !!opts.clearSetup,
2472
+ clear_target: !!opts.clearTarget,
2365
2473
  ...caseScoringPayload(opts),
2366
2474
  tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : []
2367
2475
  });
@@ -3707,7 +3815,7 @@ integrationsCmd.command("apps [query]").description("Search connectable apps (na
3707
3815
  fail(e);
3708
3816
  }
3709
3817
  });
3710
- integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass its API key or other credentials with -c, or omit them to get a browser connect URL for OAuth apps").option("-c, --credential <key=value...>", "credential field, e.g. -c api_key=\u2026 (repeatable); rejected by OAuth apps, which have no credential to pass", (v, acc) => acc.concat(v), []).option("-n, --name <name>", "display name for the connection").action(async (app, opts) => {
3818
+ integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass its API key or other credentials with -c, or omit them to get a browser connect URL for OAuth apps").option("-c, --credential <key=value...>", "credential field, e.g. -c api_key=\u2026 (repeatable); rejected by OAuth apps, which have no credential to pass", (v, acc) => acc.concat(v), []).option("-n, --name <name>", "display name for the connection").option("--rotate", "replace the credentials of the app's existing connection instead of adding a second one \u2014 for a key that was rotated at the provider; requires -c").action(async (app, opts) => {
3711
3819
  try {
3712
3820
  let credentials;
3713
3821
  if (opts.credential.length) {
@@ -3718,7 +3826,12 @@ integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass
3718
3826
  credentials[c.slice(0, eq)] = c.slice(eq + 1);
3719
3827
  }
3720
3828
  }
3721
- const res = await new ErdoClient().connectIntegration({ app, name: opts.name, credentials });
3829
+ const res = await new ErdoClient().connectIntegration({
3830
+ app,
3831
+ name: opts.name,
3832
+ credentials,
3833
+ rotate_credentials: opts.rotate || void 0
3834
+ });
3722
3835
  print(res);
3723
3836
  const notes = [];
3724
3837
  const org2 = formatOrg(res.organization_name, res.organization_slug);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.55.0",
3
+ "version": "0.55.3",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {