@erdoai/cli 0.5.1 → 0.23.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 +1241 -63
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync2 } from "fs";
5
+ import { basename } from "path";
5
6
  import { select } from "@inquirer/prompts";
6
7
  import { Command } from "commander";
7
8
 
@@ -109,26 +110,72 @@ var ErdoApiError = class extends Error {
109
110
  }
110
111
  status;
111
112
  };
113
+ var ErdoNetworkError = class extends Error {
114
+ constructor(message, cause) {
115
+ super(message);
116
+ this.cause = cause;
117
+ this.name = "ErdoNetworkError";
118
+ }
119
+ cause;
120
+ };
121
+ var NETWORK_BACKOFF_MS = [250, 1e3];
122
+ function networkErrorFor(method, path, err) {
123
+ const cause = err?.cause ?? err;
124
+ const code = cause?.code;
125
+ const causeMsg = cause?.message ?? String(cause);
126
+ const detail = code ? `${code} (${causeMsg})` : causeMsg;
127
+ return new ErdoNetworkError(`${method} ${path} failed: ${detail}`, cause);
128
+ }
129
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
112
130
  async function rawRequest(baseURL, token, orgId, method, path, body) {
113
131
  const headers = { Authorization: `Bearer ${token}` };
114
132
  if (orgId) headers["X-Organization-ID"] = orgId;
115
133
  if (body !== void 0) headers["Content-Type"] = "application/json";
116
- const res = await fetch(`${baseURL}${path}`, {
117
- method,
118
- headers,
119
- body: body !== void 0 ? JSON.stringify(body) : void 0
120
- });
121
- const text = await res.text();
122
- if (!res.ok) {
123
- let message = text;
134
+ const upper = method.toUpperCase();
135
+ const maxRetries = upper === "GET" || upper === "HEAD" ? 2 : 1;
136
+ let attempt = 0;
137
+ while (true) {
138
+ let res;
124
139
  try {
125
- const parsed = JSON.parse(text);
126
- message = parsed.message || parsed.error || text;
127
- } catch {
140
+ res = await fetch(`${baseURL}${path}`, {
141
+ method,
142
+ headers,
143
+ body: body !== void 0 ? JSON.stringify(body) : void 0
144
+ });
145
+ } catch (err) {
146
+ const netErr = networkErrorFor(method, path, err);
147
+ if (attempt < maxRetries) {
148
+ await sleep(NETWORK_BACKOFF_MS[Math.min(attempt, NETWORK_BACKOFF_MS.length - 1)]);
149
+ attempt++;
150
+ continue;
151
+ }
152
+ throw netErr;
128
153
  }
129
- throw new ErdoApiError(res.status, message || `${method} ${path} failed (${res.status})`);
154
+ const text = await res.text();
155
+ if (!res.ok) {
156
+ throw new ErdoApiError(res.status, errorMessageFor(method, path, res, text));
157
+ }
158
+ return text ? JSON.parse(text) : void 0;
159
+ }
160
+ }
161
+ function errorMessageFor(method, path, res, text) {
162
+ try {
163
+ const parsed = JSON.parse(text);
164
+ const message = parsed.message || parsed.error;
165
+ if (message) return message;
166
+ } catch {
167
+ }
168
+ if (looksLikeHTML(text)) {
169
+ const statusText = res.statusText ? ` ${res.statusText}` : "";
170
+ return `${method} ${path} was answered by a proxy, not the Erdo API (HTTP ${res.status}${statusText}). The request may still be running server-side.`;
130
171
  }
131
- return text ? JSON.parse(text) : void 0;
172
+ const trimmed = text.trim();
173
+ if (!trimmed) return `${method} ${path} failed (${res.status})`;
174
+ return trimmed.length > 500 ? `${trimmed.slice(0, 500)}\u2026` : trimmed;
175
+ }
176
+ function looksLikeHTML(text) {
177
+ const head = text.trimStart().slice(0, 200).toLowerCase();
178
+ return head.startsWith("<!doctype html") || head.startsWith("<html") || head.includes("<head>");
132
179
  }
133
180
  function fetchMe(token, orgId) {
134
181
  return rawRequest(resolveApiUrl(), token, orgId, "GET", "/v1/me");
@@ -156,6 +203,12 @@ var ErdoClient = class {
156
203
  listOrganizations() {
157
204
  return this.request("GET", "/v1/organizations");
158
205
  }
206
+ getAutonomyMode() {
207
+ return this.request("GET", "/v1/autonomy-mode");
208
+ }
209
+ setAutonomyMode(mode) {
210
+ return this.request("PUT", "/v1/autonomy-mode", { mode });
211
+ }
159
212
  listEvalSuites() {
160
213
  return this.request("GET", "/v1/evals/suites");
161
214
  }
@@ -208,6 +261,172 @@ var ErdoClient = class {
208
261
  `/v1/evals/runs/${encodeURIComponent(runID)}`
209
262
  );
210
263
  }
264
+ // --- projects ---
265
+ listProjects(params) {
266
+ const q = new URLSearchParams();
267
+ if (params?.limit) q.set("limit", String(params.limit));
268
+ if (params?.offset) q.set("offset", String(params.offset));
269
+ const qs = q.toString();
270
+ return this.request("GET", `/v1/projects${qs ? `?${qs}` : ""}`);
271
+ }
272
+ createProject(input) {
273
+ return this.request("POST", "/v1/projects", input);
274
+ }
275
+ // --- workstreams ---
276
+ listWorkstreams(status) {
277
+ const q = new URLSearchParams();
278
+ for (const s of status ?? []) q.append("status", s);
279
+ const qs = q.toString();
280
+ return this.request("GET", `/v1/workstreams${qs ? `?${qs}` : ""}`);
281
+ }
282
+ getWorkstream(slug) {
283
+ return this.request("GET", `/v1/workstreams/${encodeURIComponent(slug)}`);
284
+ }
285
+ createWorkstream(input) {
286
+ return this.request("POST", "/v1/workstreams", input);
287
+ }
288
+ updateWorkState(slug, input) {
289
+ return this.request("PATCH", `/v1/workstreams/${encodeURIComponent(slug)}/state`, input);
290
+ }
291
+ appendWorkstreamEvent(slug, body) {
292
+ return this.request(
293
+ "POST",
294
+ `/v1/workstreams/${encodeURIComponent(slug)}/events`,
295
+ { body }
296
+ );
297
+ }
298
+ listWorkstreamEvents(slug, limit) {
299
+ const qs = limit ? `?limit=${limit}` : "";
300
+ return this.request(
301
+ "GET",
302
+ `/v1/workstreams/${encodeURIComponent(slug)}/events${qs}`
303
+ );
304
+ }
305
+ addWorkstreamPhase(slug, input) {
306
+ return this.request(
307
+ "POST",
308
+ `/v1/workstreams/${encodeURIComponent(slug)}/phases`,
309
+ input
310
+ );
311
+ }
312
+ armWorkstreamLoop(slug, input) {
313
+ return this.request("POST", `/v1/workstreams/${encodeURIComponent(slug)}/arm`, input ?? {});
314
+ }
315
+ readWorkstreamLedger(slug, observationsPerExperiment) {
316
+ const qs = observationsPerExperiment ? `?observations_per_experiment=${observationsPerExperiment}` : "";
317
+ return this.request(
318
+ "GET",
319
+ `/v1/workstreams/${encodeURIComponent(slug)}/ledger${qs}`
320
+ );
321
+ }
322
+ attachWorkstreamResource(slug, input) {
323
+ return this.request(
324
+ "POST",
325
+ `/v1/workstreams/${encodeURIComponent(slug)}/resources`,
326
+ input
327
+ );
328
+ }
329
+ // --- experiments ---
330
+ listExperiments(params) {
331
+ const q = new URLSearchParams();
332
+ if (params?.workstream_slug) q.set("workstream_slug", params.workstream_slug);
333
+ if (params?.scope) q.set("scope", params.scope);
334
+ for (const s of params?.status ?? []) q.append("status", s);
335
+ const qs = q.toString();
336
+ return this.request("GET", `/v1/experiments${qs ? `?${qs}` : ""}`);
337
+ }
338
+ getExperiment(slug) {
339
+ return this.request("GET", `/v1/experiments/${encodeURIComponent(slug)}`);
340
+ }
341
+ getExperimentPolicy(slug) {
342
+ return this.request(
343
+ "GET",
344
+ `/v1/experiments/${encodeURIComponent(slug)}/policy`
345
+ );
346
+ }
347
+ createExperiment(input) {
348
+ return this.request("POST", "/v1/experiments", input);
349
+ }
350
+ updateExperiment(slug, input) {
351
+ return this.request("PATCH", `/v1/experiments/${encodeURIComponent(slug)}`, input);
352
+ }
353
+ runPersonaPanel(slug, input) {
354
+ return this.request("POST", `/v1/experiments/${encodeURIComponent(slug)}/panel`, input ?? {});
355
+ }
356
+ diagnoseExperimentPersonas(slug, input) {
357
+ return this.request("POST", `/v1/experiments/${encodeURIComponent(slug)}/diagnose`, input ?? {});
358
+ }
359
+ groundPersonas(input) {
360
+ return this.request("POST", "/v1/persona-grounding", input ?? {});
361
+ }
362
+ listExperimentObservations(slug, params) {
363
+ const q = new URLSearchParams();
364
+ if (params?.variant_key) q.set("variant_key", params.variant_key);
365
+ if (params?.observation_type) q.set("observation_type", params.observation_type);
366
+ if (params?.judge_slug) q.set("judge_slug", params.judge_slug);
367
+ if (params?.limit) q.set("limit", String(params.limit));
368
+ const qs = q.toString();
369
+ return this.request(
370
+ "GET",
371
+ `/v1/experiments/${encodeURIComponent(slug)}/observations${qs ? `?${qs}` : ""}`
372
+ );
373
+ }
374
+ // The trust dial: each judge's track record from the experiment ledger —
375
+ // paired predictions and pairwise agreement vs measured reality. The endpoint
376
+ // scopes by project + a ledger-scan limit; a judge filter is applied client-side
377
+ // over the returned rows (see the CLI command), the API has no judge parameter.
378
+ judgeCalibration(params) {
379
+ const q = new URLSearchParams();
380
+ if (params?.project_slug) q.set("project_slug", params.project_slug);
381
+ if (params?.limit) q.set("limit", String(params.limit));
382
+ const qs = q.toString();
383
+ return this.request(
384
+ "GET",
385
+ `/v1/judge-calibration${qs ? `?${qs}` : ""}`
386
+ );
387
+ }
388
+ listCrossCustomerPriors() {
389
+ return this.request("GET", "/v1/cross-customer-priors");
390
+ }
391
+ recordAllocationDecision(experimentSlug, input) {
392
+ return this.request(
393
+ "POST",
394
+ `/v1/experiments/${encodeURIComponent(experimentSlug)}/allocation-decisions`,
395
+ input
396
+ );
397
+ }
398
+ // A synchronous long call — the LLM judges every surviving pair, which can take
399
+ // minutes; the comparisons persist to the experiment ledger.
400
+ runPairwiseTournament(experimentSlug, input) {
401
+ return this.request(
402
+ "POST",
403
+ `/v1/experiments/${encodeURIComponent(experimentSlug)}/tournament`,
404
+ input
405
+ );
406
+ }
407
+ // Also synchronous and potentially slow — one prediction is written per variant
408
+ // page of the judge's kind across the org's live experiments.
409
+ backtestJudge(judgeSlug) {
410
+ return this.request("POST", "/v1/judge-backtests", { judge_slug: judgeSlug });
411
+ }
412
+ // --- attention ---
413
+ listAttentionItems(params) {
414
+ const q = new URLSearchParams();
415
+ for (const s of params?.statuses ?? []) q.append("statuses", s);
416
+ if (params?.engine_actions_only) q.set("engine_actions_only", "true");
417
+ if (params?.limit) q.set("limit", String(params.limit));
418
+ if (params?.offset) q.set("offset", String(params.offset));
419
+ const qs = q.toString();
420
+ return this.request("GET", `/v1/attention${qs ? `?${qs}` : ""}`);
421
+ }
422
+ // id may be a UUID or the item's slug — the backend accepts both.
423
+ respondAttentionItem(id, input) {
424
+ return this.request(
425
+ "POST",
426
+ `/v1/attention/${encodeURIComponent(id)}/respond`,
427
+ input
428
+ );
429
+ }
211
430
  // --- agents ---
212
431
  ask(input) {
213
432
  return this.request("POST", "/v1/ask", input);
@@ -261,6 +480,9 @@ var ErdoClient = class {
261
480
  listThreads() {
262
481
  return this.request("GET", "/v1/threads");
263
482
  }
483
+ getThreadMessages(threadId) {
484
+ return this.request("GET", `/v1/threads/${encodeURIComponent(threadId)}/messages`);
485
+ }
264
486
  // --- datasets ---
265
487
  listDatasets() {
266
488
  return this.request(
@@ -274,6 +496,75 @@ var ErdoClient = class {
274
496
  timezone
275
497
  });
276
498
  }
499
+ uploadDatasetFile(body) {
500
+ return this.request("POST", "/v1/datasets-upload", body);
501
+ }
502
+ createIntegrationDataset(body) {
503
+ return this.request(
504
+ "POST",
505
+ "/v1/integration-datasets",
506
+ body
507
+ );
508
+ }
509
+ // --- integrations ---
510
+ discoverIntegrationTables(integration, schemaName) {
511
+ const qs = schemaName ? `?schema_name=${encodeURIComponent(schemaName)}` : "";
512
+ return this.request("GET", `/v1/integrations/${encodeURIComponent(integration)}/tables${qs}`);
513
+ }
514
+ listIntegrations() {
515
+ return this.request("GET", "/v1/integrations");
516
+ }
517
+ searchIntegrationApps(query) {
518
+ const qs = query ? `?query=${encodeURIComponent(query)}` : "";
519
+ return this.request("GET", `/v1/integration-apps${qs}`);
520
+ }
521
+ connectIntegration(body) {
522
+ return this.request("POST", "/v1/integrations-connect", body);
523
+ }
524
+ checkIntegrationConnection(app) {
525
+ return this.request("GET", `/v1/integrations-connect/${encodeURIComponent(app)}`);
526
+ }
527
+ // --- dataset filters ---
528
+ listDatasetFilters(datasetSlug) {
529
+ return this.request(
530
+ "GET",
531
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/filters`
532
+ );
533
+ }
534
+ createDatasetFilter(datasetSlug, body) {
535
+ return this.request(
536
+ "POST",
537
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/filters`,
538
+ body
539
+ );
540
+ }
541
+ deleteDatasetFilter(datasetSlug, filterId) {
542
+ return this.request(
543
+ "DELETE",
544
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/filters/${encodeURIComponent(filterId)}`
545
+ );
546
+ }
547
+ // --- event pipelines ---
548
+ listEventPipelines(limit, offset) {
549
+ const q = new URLSearchParams();
550
+ if (limit) q.set("limit", String(limit));
551
+ if (offset) q.set("offset", String(offset));
552
+ const qs = q.toString();
553
+ return this.request("GET", `/v1/event-pipelines${qs ? `?${qs}` : ""}`);
554
+ }
555
+ getEventPipeline(slug) {
556
+ return this.request("GET", `/v1/event-pipelines/${encodeURIComponent(slug)}`);
557
+ }
558
+ listEventPipelineExecutions(slug, limit, offset) {
559
+ const q = new URLSearchParams();
560
+ if (limit) q.set("limit", String(limit));
561
+ if (offset) q.set("offset", String(offset));
562
+ const qs = q.toString();
563
+ return this.request(
564
+ "GET",
565
+ `/v1/event-pipelines/${encodeURIComponent(slug)}/executions${qs ? `?${qs}` : ""}`
566
+ );
567
+ }
277
568
  // --- knowledge ---
278
569
  listKnowledge() {
279
570
  return this.request("GET", "/v1/knowledge");
@@ -283,6 +574,9 @@ var ErdoClient = class {
283
574
  if (limit) params.set("limit", String(limit));
284
575
  return this.request("GET", `/v1/knowledge-search?${params.toString()}`);
285
576
  }
577
+ setKnowledgeVisibility(id, visibility) {
578
+ return this.request("PATCH", `/v1/knowledge/${id}/visibility`, { visibility });
579
+ }
286
580
  // --- automations (heartbeats) ---
287
581
  listHeartbeats() {
288
582
  return this.request(
@@ -331,6 +625,24 @@ var ErdoClient = class {
331
625
  getAgentRun(id) {
332
626
  return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
333
627
  }
628
+ // --- approvals ---
629
+ listApprovals(opts) {
630
+ const params = new URLSearchParams();
631
+ if (opts.status) params.set("status", opts.status);
632
+ if (opts.limit) params.set("limit", String(opts.limit));
633
+ const qs = params.toString();
634
+ return this.request(
635
+ "GET",
636
+ `/v1/approvals${qs ? `?${qs}` : ""}`
637
+ );
638
+ }
639
+ decideApproval(id, input) {
640
+ return this.request(
641
+ "POST",
642
+ `/v1/approvals/${encodeURIComponent(id)}/decide`,
643
+ input
644
+ );
645
+ }
334
646
  // --- pages / artifacts ---
335
647
  deployPage(input) {
336
648
  return this.request("POST", "/v1/pages", input);
@@ -365,9 +677,10 @@ import { spawn } from "child_process";
365
677
  import { randomBytes } from "crypto";
366
678
  import { createServer } from "http";
367
679
  import { hostname } from "os";
680
+ import * as readline from "readline";
368
681
  var REDIRECT_URI = "http://localhost:8080/callback";
369
682
  var PORT = 8080;
370
- var LOGIN_TIMEOUT_MS = 3 * 60 * 1e3;
683
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
371
684
  function openBrowser(url) {
372
685
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
373
686
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -376,9 +689,56 @@ function openBrowser(url) {
376
689
  } catch {
377
690
  }
378
691
  }
692
+ function copyToClipboard(text) {
693
+ const b64 = Buffer.from(text, "utf8").toString("base64");
694
+ let osc = `\x1B]52;c;${b64}\x07`;
695
+ if (process.env.TMUX) osc = `\x1BPtmux;${osc.replace(/\x1b/g, "\x1B\x1B")}\x1B\\`;
696
+ process.stdout.write(osc);
697
+ const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip";
698
+ const args = process.platform === "linux" ? ["-selection", "clipboard"] : [];
699
+ try {
700
+ const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
701
+ child.on("error", () => {
702
+ });
703
+ child.stdin.end(text);
704
+ child.unref();
705
+ } catch {
706
+ }
707
+ }
708
+ function parsePastedCode(input, state) {
709
+ const text = input.trim();
710
+ if (!text) return null;
711
+ if (text.includes("code=")) {
712
+ let params;
713
+ try {
714
+ params = new URL(text).searchParams;
715
+ } catch {
716
+ params = new URLSearchParams(text.replace(/^[^?]*\?/, ""));
717
+ }
718
+ const code = params.get("code");
719
+ if (!code) return { error: "that looks like a callback URL but has no code parameter" };
720
+ const gotState = params.get("state");
721
+ if (!gotState || gotState !== state) return { error: "state mismatch \u2014 copy the URL from THIS login attempt's tab" };
722
+ return { code };
723
+ }
724
+ if (/^[A-Za-z0-9_-]{20,}$/.test(text)) return { code: text };
725
+ return { error: "couldn't find an authorization code in that \u2014 paste the full localhost callback URL" };
726
+ }
379
727
  function waitForCode(state) {
380
728
  return new Promise((resolve, reject) => {
381
- const server = createServer((req, res) => {
729
+ let settled = false;
730
+ let server;
731
+ let rl;
732
+ const finish = (fn) => {
733
+ if (settled) return;
734
+ settled = true;
735
+ server?.close();
736
+ if (rl) rl.close();
737
+ if (process.stdin.isTTY) process.stdin.setRawMode?.(false);
738
+ process.stdin.pause();
739
+ fn();
740
+ };
741
+ server = createServer((req, res) => {
382
742
  const u = new URL(req.url || "", REDIRECT_URI);
383
743
  if (u.pathname !== "/callback") {
384
744
  res.writeHead(404);
@@ -391,16 +751,17 @@ function waitForCode(state) {
391
751
  res.end(
392
752
  '<html><head><meta charset="utf-8"></head><body><h2>Erdo CLI \u2014 authenticated. You can close this tab.</h2></body></html>'
393
753
  );
394
- server.close();
395
- if (gotState !== state) return reject(new Error("state mismatch \u2014 login aborted"));
396
- if (!code) return reject(new Error("no authorization code returned"));
397
- resolve(code);
754
+ if (gotState !== state) return finish(() => reject(new Error("state mismatch \u2014 login aborted")));
755
+ if (!code) return finish(() => reject(new Error("no authorization code returned")));
756
+ finish(() => resolve(code));
398
757
  });
399
758
  server.on("error", (err) => {
400
759
  if (err.code === "EADDRINUSE") {
401
- reject(new Error(`port ${PORT} is in use \u2014 free it and retry (or use 'erdo login --key')`));
760
+ finish(
761
+ () => reject(new Error(`port ${PORT} is in use \u2014 free it and retry (or use 'erdo login --key')`))
762
+ );
402
763
  } else {
403
- reject(err);
764
+ finish(() => reject(err));
404
765
  }
405
766
  });
406
767
  server.listen(PORT, () => {
@@ -409,12 +770,58 @@ function waitForCode(state) {
409
770
  const authURL = `${api}/auth/cli?response_type=code&client_id=erdo-cli&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&state=${state}&token_name=${encodeURIComponent(tokenName)}`;
410
771
  console.log(`Opening your browser to authorize\u2026
411
772
  If it doesn't open, visit:
412
- ${authURL}`);
773
+ ${authURL}
774
+ `);
775
+ if (process.stdin.isTTY) {
776
+ console.log(
777
+ "Working over ssh? Press c to copy the URL to your local clipboard, open it\nin your local browser, then paste the localhost callback URL (from the tab\nthat fails to load) here and press Enter."
778
+ );
779
+ }
413
780
  openBrowser(authURL);
781
+ if (process.stdin.isTTY) {
782
+ readline.emitKeypressEvents(process.stdin);
783
+ process.stdin.setRawMode?.(true);
784
+ process.stdin.resume();
785
+ let buffer = "";
786
+ process.stdin.on("keypress", (str, key) => {
787
+ if (settled) return;
788
+ if (key?.ctrl && key.name === "c") {
789
+ finish(() => reject(new Error("login cancelled")));
790
+ return;
791
+ }
792
+ if (key?.name === "return" || key?.name === "enter") {
793
+ const parsed = parsePastedCode(buffer, state);
794
+ buffer = "";
795
+ process.stdout.write("\n");
796
+ if (!parsed) return;
797
+ if ("error" in parsed) {
798
+ console.log(` \u2717 ${parsed.error}`);
799
+ return;
800
+ }
801
+ finish(() => resolve(parsed.code));
802
+ return;
803
+ }
804
+ if (key?.name === "backspace") {
805
+ if (buffer.length > 0) {
806
+ buffer = buffer.slice(0, -1);
807
+ process.stdout.write("\b \b");
808
+ }
809
+ return;
810
+ }
811
+ if ((str === "c" || str === "C") && buffer.length === 0) {
812
+ copyToClipboard(authURL);
813
+ console.log(" \u2713 copied the login URL to your clipboard");
814
+ return;
815
+ }
816
+ if (str && !key?.ctrl && !key?.meta) {
817
+ buffer += str;
818
+ process.stdout.write(str);
819
+ }
820
+ });
821
+ }
414
822
  });
415
823
  setTimeout(() => {
416
- server.close();
417
- reject(new Error("login timed out after 3 minutes"));
824
+ finish(() => reject(new Error("login timed out after 5 minutes")));
418
825
  }, LOGIN_TIMEOUT_MS).unref();
419
826
  });
420
827
  }
@@ -508,10 +915,29 @@ function fail(err) {
508
915
  }
509
916
  process.exit(1);
510
917
  }
918
+ function awaitingApprovalMessage(threadID) {
919
+ return [
920
+ "the run paused for approval.",
921
+ " erdo approvals list --status pending",
922
+ " erdo approvals decide <id> --approve",
923
+ ` erdo agent messages ${threadID}`
924
+ ].join("\n");
925
+ }
926
+ function timedOutMessage(threadID) {
927
+ return [
928
+ "timed out waiting for the run to finish \u2014 it is still running on its thread.",
929
+ ` erdo agent messages ${threadID}`,
930
+ ` erdo runs list --thread ${threadID}`
931
+ ].join("\n");
932
+ }
511
933
  function print(value) {
512
934
  console.log(JSON.stringify(value, null, 2));
513
935
  }
514
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
936
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
937
+ var collect = (v, acc) => {
938
+ acc.push(v);
939
+ return acc;
940
+ };
515
941
  function summariseResults(results, cases = []) {
516
942
  const nameByID = new Map(cases.map((c) => [c.id, c.name]));
517
943
  for (const r of results) {
@@ -669,6 +1095,20 @@ org.command("use [idOrSlug]").description("Set the active org for the current ac
669
1095
  fail(e);
670
1096
  }
671
1097
  });
1098
+ org.command("autonomy [mode]").description("Show or set the org's engine-autonomy mode (autopilot | propose | strict)").action(async (mode) => {
1099
+ try {
1100
+ const api = new ErdoClient();
1101
+ if (mode) {
1102
+ const { mode: updated } = await api.setAutonomyMode(mode);
1103
+ console.log(`Autonomy mode set to: ${updated}`);
1104
+ } else {
1105
+ const { mode: current } = await api.getAutonomyMode();
1106
+ console.log(`Autonomy mode: ${current}`);
1107
+ }
1108
+ } catch (e) {
1109
+ fail(e);
1110
+ }
1111
+ });
672
1112
  var evalCmd = program.command("eval").description("Run and manage evals");
673
1113
  evalCmd.command("suites").description("List eval suites in the active org (+ global)").action(async () => {
674
1114
  try {
@@ -687,6 +1127,475 @@ evalCmd.command("suite <slug>").description("Show a suite and its cases").action
687
1127
  fail(e);
688
1128
  }
689
1129
  });
1130
+ var projectCmd = program.command("project").description("Projects \u2014 the container workstreams and experiments live in");
1131
+ projectCmd.command("list").description("List projects in the active org").option("-n, --limit <n>", "max projects", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).action(async (opts) => {
1132
+ try {
1133
+ print(await new ErdoClient().listProjects({ limit: opts.limit, offset: opts.offset }));
1134
+ } catch (e) {
1135
+ fail(e);
1136
+ }
1137
+ });
1138
+ projectCmd.command("create").description("Create a project in the active org").requiredOption("--slug <slug>", "URL-safe slug, unique per org").requiredOption("--name <name>", "human-readable name").option("--description <text>", "free-form description").action(async (opts) => {
1139
+ try {
1140
+ print(
1141
+ await new ErdoClient().createProject({
1142
+ slug: opts.slug,
1143
+ name: opts.name,
1144
+ description: opts.description
1145
+ })
1146
+ );
1147
+ } catch (e) {
1148
+ fail(e);
1149
+ }
1150
+ });
1151
+ var wsCmd = program.command("workstream").description("Drive and observe Workstreams");
1152
+ wsCmd.command("list").description("List workstreams in the active org").option("-s, --status <status...>", "filter by status (active, awaiting_user, blocked, completed)").action(async (opts) => {
1153
+ try {
1154
+ print(await new ErdoClient().listWorkstreams(opts.status));
1155
+ } catch (e) {
1156
+ fail(e);
1157
+ }
1158
+ });
1159
+ wsCmd.command("get <slug>").description("Show a workstream \u2014 status, phases, resources, recent log").action(async (slug) => {
1160
+ try {
1161
+ print(await new ErdoClient().getWorkstream(slug));
1162
+ } catch (e) {
1163
+ fail(e);
1164
+ }
1165
+ });
1166
+ wsCmd.command("create").description("Create a workstream in a project").requiredOption("--project <slug>", "project slug").requiredOption("--slug <slug>", "workstream slug (unique per org)").requiredOption("--title <title>", "human-readable title").option("--description <text>", "free-form description").action(async (opts) => {
1167
+ try {
1168
+ print(
1169
+ await new ErdoClient().createWorkstream({
1170
+ project_slug: opts.project,
1171
+ slug: opts.slug,
1172
+ title: opts.title,
1173
+ description: opts.description
1174
+ })
1175
+ );
1176
+ } catch (e) {
1177
+ fail(e);
1178
+ }
1179
+ });
1180
+ wsCmd.command("log <slug> <body...>").description("Append a line to the workstream's event log").action(async (slug, body) => {
1181
+ try {
1182
+ print(await new ErdoClient().appendWorkstreamEvent(slug, body.join(" ")));
1183
+ } catch (e) {
1184
+ fail(e);
1185
+ }
1186
+ });
1187
+ wsCmd.command("events <slug>").description("Show a workstream's full event log (newest first)").option("-n, --limit <n>", "max events", (v) => parseInt(v, 10)).action(async (slug, opts) => {
1188
+ try {
1189
+ print(await new ErdoClient().listWorkstreamEvents(slug, opts.limit));
1190
+ } catch (e) {
1191
+ fail(e);
1192
+ }
1193
+ });
1194
+ wsCmd.command("phase-add <slug>").description("Append a new phase to a workstream").requiredOption("--phase-slug <phaseSlug>", "URL-safe phase id, unique within the workstream").requiredOption("--title <title>", "human-readable phase title").option("--skill <skillSlug>", "optional skill that runs this phase").action(
1195
+ async (slug, opts) => {
1196
+ try {
1197
+ print(
1198
+ await new ErdoClient().addWorkstreamPhase(slug, {
1199
+ slug: opts.phaseSlug,
1200
+ title: opts.title,
1201
+ skill_slug: opts.skill
1202
+ })
1203
+ );
1204
+ } catch (e) {
1205
+ fail(e);
1206
+ }
1207
+ }
1208
+ );
1209
+ wsCmd.command("set-state <slug>").description("Update a workstream's status / title / description").option("--status <status>", "active, awaiting_user, blocked, completed").option("--title <title>", "retitle").option("--description <text>", "describe current state / surface a blocker").option("--summary <text>", "one-line audit-log summary of this update").action(
1210
+ async (slug, opts) => {
1211
+ try {
1212
+ print(
1213
+ await new ErdoClient().updateWorkState(slug, {
1214
+ status: opts.status,
1215
+ title: opts.title,
1216
+ description: opts.description,
1217
+ summary: opts.summary
1218
+ })
1219
+ );
1220
+ } catch (e) {
1221
+ fail(e);
1222
+ }
1223
+ }
1224
+ );
1225
+ wsCmd.command("arm <slug>").description("Arm the recurring reconciliation loop (allocator pass) for a workstream").option("--interval <minutes>", "how often the loop runs", (v) => parseInt(v, 10)).option("--model <id>", "model the loop runs on").option("--timezone <tz>", "timezone for scheduling").option("--token-budget <millicents>", "total spend ceiling in millicents (tenths of a cent)", (v) => parseInt(v, 10)).option("--escalation-budget <n>", "max escalations to a human per day", (v) => parseInt(v, 10)).action(
1226
+ async (slug, opts) => {
1227
+ try {
1228
+ const client = new ErdoClient();
1229
+ if (opts.tokenBudget !== void 0 || opts.escalationBudget !== void 0) {
1230
+ await client.updateWorkState(slug, {
1231
+ token_budget_millicents: opts.tokenBudget,
1232
+ escalation_budget_per_day: opts.escalationBudget
1233
+ });
1234
+ }
1235
+ print(
1236
+ await client.armWorkstreamLoop(slug, {
1237
+ interval_minutes: opts.interval,
1238
+ model: opts.model,
1239
+ timezone: opts.timezone
1240
+ })
1241
+ );
1242
+ } catch (e) {
1243
+ fail(e);
1244
+ }
1245
+ }
1246
+ );
1247
+ wsCmd.command("ledger <slug>").description(
1248
+ "Read the allocator's single ledger view \u2014 budget, experiments + observations, judge calibration, attention items, allocator recommendation"
1249
+ ).option("--observations <n>", "observations to include per experiment", (v) => parseInt(v, 10)).action(async (slug, opts) => {
1250
+ try {
1251
+ print(await new ErdoClient().readWorkstreamLedger(slug, opts.observations));
1252
+ } catch (e) {
1253
+ fail(e);
1254
+ }
1255
+ });
1256
+ wsCmd.command("attach <slug>").description("Link a resource (page, dataset, heartbeat, experiment_run, \u2026) to a workstream").requiredOption("--type <resourceType>", "resource type (page, dataset, heartbeat, experiment_run, \u2026)").requiredOption("--id <resourceId>", "the resource's UUID").requiredOption("--rel <relationshipType>", "output | input | evidence | monitoring | subject | parent | child | running").option("--title <title>", "optional label for the link").option("--phase <phaseId>", "optional phase to attach under").action(
1257
+ async (slug, opts) => {
1258
+ try {
1259
+ print(
1260
+ await new ErdoClient().attachWorkstreamResource(slug, {
1261
+ resource_type: opts.type,
1262
+ resource_id: opts.id,
1263
+ relationship_type: opts.rel,
1264
+ title: opts.title,
1265
+ phase_id: opts.phase
1266
+ })
1267
+ );
1268
+ } catch (e) {
1269
+ fail(e);
1270
+ }
1271
+ }
1272
+ );
1273
+ function parseVariant(spec) {
1274
+ const v = {};
1275
+ for (const raw of spec.split(",")) {
1276
+ const part = raw.trim();
1277
+ if (!part) continue;
1278
+ const eq = part.indexOf("=");
1279
+ if (eq === -1) {
1280
+ if (part === "control" || part === "is_control") {
1281
+ v.is_control = true;
1282
+ continue;
1283
+ }
1284
+ throw new Error(`--variant: unknown flag "${part}" (expected key=, label=, page=, alloc=, class=, or 'control')`);
1285
+ }
1286
+ const k = part.slice(0, eq).trim();
1287
+ const val = part.slice(eq + 1).trim();
1288
+ switch (k) {
1289
+ case "key":
1290
+ v.key = val;
1291
+ break;
1292
+ case "label":
1293
+ v.label = val;
1294
+ break;
1295
+ case "alloc":
1296
+ case "allocation_percent":
1297
+ v.allocation_percent = parseInt(val, 10);
1298
+ if (isNaN(v.allocation_percent)) throw new Error(`--variant alloc= must be an integer, got: ${val}`);
1299
+ break;
1300
+ case "control":
1301
+ case "is_control":
1302
+ v.is_control = val !== "false";
1303
+ break;
1304
+ case "page":
1305
+ v.treatment_resource_type = "page";
1306
+ v.treatment_resource_id = val;
1307
+ break;
1308
+ case "type":
1309
+ case "treatment_resource_type":
1310
+ v.treatment_resource_type = val;
1311
+ break;
1312
+ case "resource":
1313
+ case "treatment_resource_id":
1314
+ v.treatment_resource_id = val;
1315
+ break;
1316
+ case "class":
1317
+ case "treatment_class":
1318
+ v.treatment_class = val;
1319
+ break;
1320
+ default:
1321
+ throw new Error(`--variant: unknown key "${k}"`);
1322
+ }
1323
+ }
1324
+ if (!v.key) {
1325
+ throw new Error('--variant requires key=, e.g. --variant "key=control,label=Current,control"');
1326
+ }
1327
+ if (!v.label) v.label = v.key;
1328
+ return v;
1329
+ }
1330
+ function parseEvidence(spec) {
1331
+ const s = spec.trim();
1332
+ if (!s.includes("=")) {
1333
+ const idx = s.lastIndexOf(":");
1334
+ const slug2 = idx === -1 ? s : s.slice(0, idx);
1335
+ const role2 = idx === -1 ? "primary" : s.slice(idx + 1);
1336
+ if (!slug2.trim()) throw new Error("--evidence requires a dataset slug");
1337
+ return { dataset_slug: slug2.trim(), role: (role2 || "primary").trim() };
1338
+ }
1339
+ let slug = "";
1340
+ let role = "primary";
1341
+ for (const raw of s.split(",")) {
1342
+ const part = raw.trim();
1343
+ if (!part) continue;
1344
+ const eq = part.indexOf("=");
1345
+ if (eq === -1) throw new Error(`--evidence: expected key=value pair, got "${part}"`);
1346
+ const k = part.slice(0, eq).trim();
1347
+ const val = part.slice(eq + 1).trim();
1348
+ if (k === "slug" || k === "dataset" || k === "dataset_slug") slug = val;
1349
+ else if (k === "role") role = val;
1350
+ else throw new Error(`--evidence: unknown key "${k}"`);
1351
+ }
1352
+ if (!slug) throw new Error('--evidence requires slug=, e.g. --evidence "slug=my-leads,role=primary"');
1353
+ return { dataset_slug: slug, role };
1354
+ }
1355
+ var expCmd = program.command("experiment").description("Drive and observe Experiments");
1356
+ expCmd.command("list").description("List experiment runs").option("-w, --workstream <slug>", "filter to one workstream").option("--scope <scope>", "local, platform_canary, platform_global").option("-s, --status <status...>", "filter by status (planned, running, reading, decided)").action(async (opts) => {
1357
+ try {
1358
+ print(
1359
+ await new ErdoClient().listExperiments({
1360
+ workstream_slug: opts.workstream,
1361
+ scope: opts.scope,
1362
+ status: opts.status
1363
+ })
1364
+ );
1365
+ } catch (e) {
1366
+ fail(e);
1367
+ }
1368
+ });
1369
+ expCmd.command("get <slug>").description("Show an experiment \u2014 hypothesis, variants, evidence, status, decision").action(async (slug) => {
1370
+ try {
1371
+ print(await new ErdoClient().getExperiment(slug));
1372
+ } catch (e) {
1373
+ fail(e);
1374
+ }
1375
+ });
1376
+ expCmd.command("policy <slug>").description(
1377
+ "Show the decision policy state \u2014 per-variant posteriors, allocation, win frequencies, stop probabilities, calibration gate, recommended decision, and HAIPW effect readout"
1378
+ ).action(async (slug) => {
1379
+ try {
1380
+ print(await new ErdoClient().getExperimentPolicy(slug));
1381
+ } catch (e) {
1382
+ fail(e);
1383
+ }
1384
+ });
1385
+ 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(
1386
+ "--variant <spec>",
1387
+ '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',
1388
+ collect,
1389
+ []
1390
+ ).option(
1391
+ "--evidence <spec>",
1392
+ 'a measurement dataset "slug=<dataset>,role=primary" or "<dataset-slug>:<role>" (repeatable)',
1393
+ collect,
1394
+ []
1395
+ ).action(
1396
+ async (opts) => {
1397
+ try {
1398
+ const variants = opts.variant.map(parseVariant);
1399
+ const evidence_datasets = opts.evidence.map(parseEvidence);
1400
+ print(
1401
+ await new ErdoClient().createExperiment({
1402
+ project_slug: opts.project,
1403
+ slug: opts.slug,
1404
+ title: opts.title,
1405
+ workstream_slug: opts.workstream,
1406
+ scope: opts.scope,
1407
+ hypothesis_markdown: opts.hypothesis,
1408
+ primary_metric: opts.primaryMetric,
1409
+ decision_rule_markdown: opts.decisionRule,
1410
+ guardrail_metrics: opts.guardrail.length ? opts.guardrail : void 0,
1411
+ variants: variants.length ? variants : void 0,
1412
+ evidence_datasets: evidence_datasets.length ? evidence_datasets : void 0
1413
+ })
1414
+ );
1415
+ } catch (e) {
1416
+ fail(e);
1417
+ }
1418
+ }
1419
+ );
1420
+ expCmd.command("observations <slug>").description("Read an experiment's observation log (metric reads / decisions / actions / judge forecasts)").option("--variant <key>", "filter to one variant").option("--type <type>", "metric_read, decision_check, action, prediction, comparison").option("--judge <slug>", "filter to one judge's forecasts within this experiment").option("-n, --limit <n>", "max rows", (v) => parseInt(v, 10)).action(
1421
+ async (slug, opts) => {
1422
+ try {
1423
+ print(
1424
+ await new ErdoClient().listExperimentObservations(slug, {
1425
+ variant_key: opts.variant,
1426
+ observation_type: opts.type,
1427
+ judge_slug: opts.judge,
1428
+ limit: opts.limit
1429
+ })
1430
+ );
1431
+ } catch (e) {
1432
+ fail(e);
1433
+ }
1434
+ }
1435
+ );
1436
+ expCmd.command("priors").description(
1437
+ "List the platform's cross-customer prior library \u2014 the aggregate effect of each treatment class (headline_change, cta_change, \u2026) on each page metric, pooled across all contributing orgs. Aggregate-only (\u22655 experiments, \u22653 orgs per cell); nothing identifies any org, page, or result."
1438
+ ).action(async () => {
1439
+ try {
1440
+ print(await new ErdoClient().listCrossCustomerPriors());
1441
+ } catch (e) {
1442
+ fail(e);
1443
+ }
1444
+ });
1445
+ expCmd.command("panel <slug>").description("Run the persona panel (wind tunnel): brand-brief personas predict each page variant's micro-conversions before paid traffic decides").option("--persona <slug...>", "restrict to these persona skill slugs (default: every approved persona)").option("--variant <key...>", "restrict to these variant keys (default: every page variant)").option("--draws <n>", "K samples per persona\xD7variant averaged into the prediction (default 5)", (v) => parseInt(v, 10)).option("--wait", "poll until the panel's ranking (decision_check) lands, then print it").action(
1446
+ async (slug, opts) => {
1447
+ try {
1448
+ const client = new ErdoClient();
1449
+ const started = await client.runPersonaPanel(slug, {
1450
+ persona_slugs: opts.persona,
1451
+ variant_keys: opts.variant,
1452
+ draws: opts.draws
1453
+ });
1454
+ print(started);
1455
+ if (!opts.wait) return;
1456
+ const deadline = Date.now() + 20 * 60 * 1e3;
1457
+ process.stderr.write("waiting for the panel ranking");
1458
+ while (Date.now() < deadline) {
1459
+ await new Promise((r) => setTimeout(r, 5e3));
1460
+ process.stderr.write(".");
1461
+ const res = await client.listExperimentObservations(slug, {
1462
+ observation_type: "decision_check",
1463
+ limit: 20
1464
+ });
1465
+ for (const obs of res.observations ?? []) {
1466
+ const details = typeof obs.details === "string" ? JSON.parse(obs.details) : obs.details;
1467
+ if (details && details.panel_run_id === started.panel_run_id) {
1468
+ process.stderr.write("\n");
1469
+ print({ ranking: details.ranking, kill_list: details.kill_list ?? [], primary_metric: details.primary_metric, failed_cells: details.failed_cells ?? [] });
1470
+ return;
1471
+ }
1472
+ }
1473
+ }
1474
+ process.stderr.write("\n");
1475
+ fail(new Error(`panel still running after 20m \u2014 check later with: erdo experiment observations ${slug} --type decision_check`));
1476
+ } catch (e) {
1477
+ fail(e);
1478
+ }
1479
+ }
1480
+ );
1481
+ expCmd.command("diagnose <slug>").description(
1482
+ "Run wind-tunnel Stage B: browser sessions diagnose WHERE and WHY each persona bails on the surviving variants (never estimates \u2014 run the panel first for the ranking)"
1483
+ ).option("--top-k <n>", "how many surviving variants get sessions (default 3)", (v) => parseInt(v, 10)).option("--survivor <key...>", "survival ranking, best first (default: the latest panel's decision_check ranking)").option("--persona <slug...>", "restrict to these persona skill slugs").action(
1484
+ async (slug, opts) => {
1485
+ try {
1486
+ const ack = await new ErdoClient().diagnoseExperimentPersonas(slug, {
1487
+ top_k: opts.topK,
1488
+ survivor_variant_keys: opts.survivor,
1489
+ persona_slugs: opts.persona
1490
+ });
1491
+ print(ack);
1492
+ process.stderr.write(
1493
+ `sessions run in the background; read the narratives with: erdo experiment observations ${slug} --type action_taken
1494
+ `
1495
+ );
1496
+ } catch (e) {
1497
+ fail(e);
1498
+ }
1499
+ }
1500
+ );
1501
+ expCmd.command("decide <slug>").description("Record an experiment's decision (ship/stop/iterate/inconclusive)").requiredOption("--decision <decision>", "ship, stop, iterate, inconclusive").option("--outcome <text>", "the narrative result").action(async (slug, opts) => {
1502
+ try {
1503
+ print(
1504
+ await new ErdoClient().updateExperiment(slug, {
1505
+ status: "decided",
1506
+ decision: opts.decision,
1507
+ outcome_markdown: opts.outcome
1508
+ })
1509
+ );
1510
+ } catch (e) {
1511
+ fail(e);
1512
+ }
1513
+ });
1514
+ expCmd.command("set-status <slug> <status>").description("Set an experiment's status (planned, running, reading, decided)").action(async (slug, status) => {
1515
+ try {
1516
+ print(await new ErdoClient().updateExperiment(slug, { status }));
1517
+ } catch (e) {
1518
+ fail(e);
1519
+ }
1520
+ });
1521
+ expCmd.command("calibration").description(
1522
+ "Read each judge's track record \u2014 paired predictions and pairwise agreement vs measured reality (the trust dial)"
1523
+ ).option("--project <slug>", "limit to one project (default: every project you can view)").option("--judge <slug>", "show only judges whose slug contains this text").option("-n, --limit <n>", "max ledger rows scanned (default 5000)", (v) => parseInt(v, 10)).action(async (opts) => {
1524
+ try {
1525
+ const res = await new ErdoClient().judgeCalibration({
1526
+ project_slug: opts.project,
1527
+ limit: opts.limit
1528
+ });
1529
+ if (opts.judge && Array.isArray(res.judges)) {
1530
+ const needle = opts.judge.toLowerCase();
1531
+ res.judges = res.judges.filter((j) => j.judge_slug.toLowerCase().includes(needle));
1532
+ }
1533
+ print(res);
1534
+ } catch (e) {
1535
+ fail(e);
1536
+ }
1537
+ });
1538
+ expCmd.command("record-decision <slug>").description(
1539
+ "Record that you are overriding the allocator's recommendation for one bet (calibration data on the allocator)"
1540
+ ).requiredOption("--variant <key>", "the variant/bet you are deciding on").requiredOption(
1541
+ "--recommended <action>",
1542
+ "what the allocator recommended: fund | hold | kill_candidate | lottery_slot | champion_trial"
1543
+ ).requiredOption("--chosen <action>", "the action you chose instead").requiredOption("--reason <text>", "why you overrode the recommendation").action(
1544
+ async (slug, opts) => {
1545
+ try {
1546
+ print(
1547
+ await new ErdoClient().recordAllocationDecision(slug, {
1548
+ variant_key: opts.variant,
1549
+ recommended_action: opts.recommended,
1550
+ chosen_action: opts.chosen,
1551
+ reason: opts.reason
1552
+ })
1553
+ );
1554
+ } catch (e) {
1555
+ fail(e);
1556
+ }
1557
+ }
1558
+ );
1559
+ expCmd.command("tournament <slug>").description(
1560
+ "Coalesce an experiment's variants: cluster near-dups, judge survivors pairwise, fit a Bradley\u2013Terry ranking; comparisons persist to the ledger"
1561
+ ).requiredOption("--criteria <text>", "the criteria the judge ranks pairs on").option("--kind <kind>", "restrict to variants of this artifact kind").option("--variant <key>", "restrict to these variant keys (repeatable)", collect, []).option("--max-comparisons <n>", "cap on pairwise comparisons", (v) => parseInt(v, 10)).action(
1562
+ async (slug, opts) => {
1563
+ try {
1564
+ process.stderr.write("judging pairs \u2014 this can take a few minutes\u2026\n");
1565
+ print(
1566
+ await new ErdoClient().runPairwiseTournament(slug, {
1567
+ criteria: opts.criteria,
1568
+ kind: opts.kind,
1569
+ variant_keys: opts.variant.length ? opts.variant : void 0,
1570
+ max_comparisons: opts.maxComparisons
1571
+ })
1572
+ );
1573
+ } catch (e) {
1574
+ fail(e);
1575
+ }
1576
+ }
1577
+ );
1578
+ var judgeCmd = program.command("judge").description("Judges \u2014 back-tests and calibration");
1579
+ judgeCmd.command("backtest <judgeSlug>").description(
1580
+ "Run a judge over the org's live experiments' variant pages of its kind and persist one prediction per variant (earns calibration standing immediately; idempotent \u2014 already-forecast variants are skipped)"
1581
+ ).action(async (judgeSlug) => {
1582
+ try {
1583
+ process.stderr.write("back-testing \u2014 this can take a few minutes\u2026\n");
1584
+ print(await new ErdoClient().backtestJudge(judgeSlug));
1585
+ } catch (e) {
1586
+ fail(e);
1587
+ }
1588
+ });
1589
+ var personaCmd = program.command("persona").description("Ground personas in measured traffic");
1590
+ personaCmd.command("ground").description(
1591
+ "Ground the org's persona skills in measured behavior: cluster the last 28 days of page-events sessions and update each matching persona in place (silent no-op below the data gates)"
1592
+ ).option("--window-days <n>", "look-back window in days (default 28)", (v) => parseInt(v, 10)).action(async (opts) => {
1593
+ try {
1594
+ print(await new ErdoClient().groundPersonas({ window_days: opts.windowDays }));
1595
+ } catch (e) {
1596
+ fail(e);
1597
+ }
1598
+ });
690
1599
  function parseRubric(json) {
691
1600
  try {
692
1601
  return JSON.parse(json);
@@ -694,10 +1603,6 @@ function parseRubric(json) {
694
1603
  throw new Error(`--rubric must be valid JSON, e.g. '[{"criterion":"brand","weight":1}]'`);
695
1604
  }
696
1605
  }
697
- var collect = (v, acc) => {
698
- acc.push(v);
699
- return acc;
700
- };
701
1606
  function caseScoringPayload(opts) {
702
1607
  const out = {};
703
1608
  if (opts.rubric) out.rubric = parseRubric(opts.rubric);
@@ -765,7 +1670,7 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
765
1670
  console.log(`run_id: ${run_id}`);
766
1671
  if (!opts.watch) return;
767
1672
  for (; ; ) {
768
- await sleep(5e3);
1673
+ await sleep2(5e3);
769
1674
  const { run, results, cases } = await api.getEvalRun(run_id);
770
1675
  if (run.status === "running") {
771
1676
  console.log(` ...running (${results.length}/${run.total_cases} cases scored)`);
@@ -861,33 +1766,32 @@ caseCmd.command("rm <slug> <caseName>").description("Archive a case from a suite
861
1766
  });
862
1767
  var agentCmd = program.command("agent").description("Run agents");
863
1768
  agentCmd.command("ask <question>").description("Ask the data-question agent (one-shot); prints the answer").option("-d, --datasets <csv>", "dataset slugs to scope to").option(
864
- "--async",
865
- "kick off the run and poll for the result instead of holding one long request open (use for slow builds)"
1769
+ "--sync",
1770
+ "hold one long HTTP request open until the run finishes, instead of polling. Faster for quick questions; a proxy will time it out on any run over ~100s, losing the thread id while the run continues server-side"
866
1771
  ).action(async (question, opts) => {
867
1772
  try {
868
1773
  const client = new ErdoClient();
869
1774
  const res = await client.ask({
870
1775
  question,
871
1776
  dataset_slugs: opts.datasets ? opts.datasets.split(",").map((s) => s.trim()) : void 0,
872
- async: opts.async
1777
+ async: !opts.sync
873
1778
  });
874
- if (opts.async) {
875
- process.stderr.write(`thread: ${res.thread_id}
876
- running\u2026`);
877
- const run = await client.waitForThreadRun(res.thread_id, {
878
- onTick: () => process.stderr.write(".")
879
- });
880
- process.stderr.write("\n");
881
- if (!run) throw new Error("timed out waiting for the run to finish");
882
- if (run.status === "awaiting_approval")
883
- throw new Error("run needs approval \u2014 approve it in the Erdo app, then re-check the thread");
884
- if (run.error) throw new Error(run.error);
885
- console.log(run.output || `(status: ${run.status})`);
1779
+ if (opts.sync) {
1780
+ console.log(res.answer || `(status: ${res.status})`);
886
1781
  console.log(`
887
1782
  thread: ${res.thread_id}`);
888
1783
  return;
889
1784
  }
890
- console.log(res.answer || `(status: ${res.status})`);
1785
+ process.stderr.write(`thread: ${res.thread_id}
1786
+ running\u2026`);
1787
+ const run = await client.waitForThreadRun(res.thread_id, {
1788
+ onTick: () => process.stderr.write(".")
1789
+ });
1790
+ process.stderr.write("\n");
1791
+ if (!run) throw new Error(timedOutMessage(res.thread_id));
1792
+ if (run.status === "awaiting_approval") throw new Error(awaitingApprovalMessage(res.thread_id));
1793
+ if (run.error) throw new Error(run.error);
1794
+ console.log(run.output || `(status: ${run.status})`);
891
1795
  console.log(`
892
1796
  thread: ${res.thread_id}`);
893
1797
  } catch (e) {
@@ -895,33 +1799,32 @@ thread: ${res.thread_id}`);
895
1799
  }
896
1800
  });
897
1801
  agentCmd.command("send <threadId> <message>").description("Send a message to a thread and print the agent's reply").option("-a, --agent <key>", "agent to invoke (e.g. erdo.artifact-builder)").option(
898
- "--async",
899
- "kick off the run and poll for the result instead of holding one long request open (use for slow builds)"
1802
+ "--sync",
1803
+ "hold one long HTTP request open until the run finishes, instead of polling. Faster for quick questions; a proxy will time it out on any run over ~100s while the run continues server-side"
900
1804
  ).action(async (threadId, message, opts) => {
901
1805
  try {
902
1806
  const client = new ErdoClient();
903
- const baselineRunID = opts.async ? await client.latestRunID(threadId) : void 0;
1807
+ const baselineRunID = opts.sync ? void 0 : await client.latestRunID(threadId);
904
1808
  const res = await client.sendMessage(threadId, {
905
1809
  message,
906
1810
  agent_key: opts.agent,
907
- async: opts.async
1811
+ async: !opts.sync
908
1812
  });
909
1813
  if (res.error) throw new Error(res.error);
910
- if (opts.async) {
911
- process.stderr.write("running\u2026");
912
- const run = await client.waitForThreadRun(threadId, {
913
- ignoreRunID: baselineRunID,
914
- onTick: () => process.stderr.write(".")
915
- });
916
- process.stderr.write("\n");
917
- if (!run) throw new Error("timed out waiting for the run to finish");
918
- if (run.status === "awaiting_approval")
919
- throw new Error("run needs approval \u2014 approve it in the Erdo app, then re-check the thread");
920
- if (run.error) throw new Error(run.error);
921
- console.log(run.output || `(status: ${run.status})`);
1814
+ if (opts.sync) {
1815
+ console.log(res.answer || `(status: ${res.status})`);
922
1816
  return;
923
1817
  }
924
- console.log(res.answer || `(status: ${res.status})`);
1818
+ process.stderr.write("running\u2026");
1819
+ const run = await client.waitForThreadRun(threadId, {
1820
+ ignoreRunID: baselineRunID,
1821
+ onTick: () => process.stderr.write(".")
1822
+ });
1823
+ process.stderr.write("\n");
1824
+ if (!run) throw new Error(timedOutMessage(threadId));
1825
+ if (run.status === "awaiting_approval") throw new Error(awaitingApprovalMessage(threadId));
1826
+ if (run.error) throw new Error(run.error);
1827
+ console.log(run.output || `(status: ${run.status})`);
925
1828
  } catch (e) {
926
1829
  fail(e);
927
1830
  }
@@ -945,6 +1848,24 @@ agentCmd.command("threads").description("List your threads").action(async () =>
945
1848
  fail(e);
946
1849
  }
947
1850
  });
1851
+ agentCmd.command("messages <threadId>").description("Show a thread's messages (role + text)").action(async (threadId) => {
1852
+ try {
1853
+ const { messages } = await new ErdoClient().getThreadMessages(threadId);
1854
+ for (const m of messages) {
1855
+ const role = m.message.author_entity_type === "app" ? "agent" : m.message.author_entity_type;
1856
+ console.log(`\u2014 ${role} \xB7 ${m.message.created_at} \u2014`);
1857
+ for (const c of m.contents) {
1858
+ if (c.content_type === "text") {
1859
+ console.log(c.content);
1860
+ } else {
1861
+ console.log(`\x1B[2m[${c.content_type}]\x1B[0m`);
1862
+ }
1863
+ }
1864
+ }
1865
+ } catch (e) {
1866
+ fail(e);
1867
+ }
1868
+ });
948
1869
  var runsCmd = program.command("runs").description("Inspect agent runs");
949
1870
  runsCmd.command("list").description("List agent runs (filter by agent OR thread \u2014 mutually exclusive)").option("-a, --agent <key>", "filter by agent key").option("-t, --thread <id>", "filter to a thread").option("-l, --limit <n>", "max runs", (v) => parseInt(v, 10)).action(async (opts) => {
950
1871
  try {
@@ -961,6 +1882,88 @@ runsCmd.command("get <id>").description("Show an agent run (status, output, trac
961
1882
  fail(e);
962
1883
  }
963
1884
  });
1885
+ var approvalsCmd = program.command("approvals").description("List and decide approval requests (actions agents paused on)");
1886
+ approvalsCmd.command("list").description("List approval requests, optionally filtered by status").option("-s, --status <status>", "pending | approved | rejected | expired (default: all)").option("-l, --limit <n>", "max requests", (v) => parseInt(v, 10)).option("--json", "output raw JSON").action(async (opts) => {
1887
+ try {
1888
+ const res = await new ErdoClient().listApprovals(opts);
1889
+ if (opts.json) {
1890
+ print(res);
1891
+ return;
1892
+ }
1893
+ for (const r of res.requests) {
1894
+ console.log(
1895
+ `${r.id} ${r.status} ${r.action_key} ${r.action_display} ${r.created_at}`
1896
+ );
1897
+ }
1898
+ } catch (e) {
1899
+ fail(e);
1900
+ }
1901
+ });
1902
+ approvalsCmd.command("decide <id>").description("Approve or reject a pending approval request").option("--approve", "approve the request").option("--reject", "reject the request").option(
1903
+ "--scope <scope>",
1904
+ "once (default) | always_this_job | always_org | always_user",
1905
+ "once"
1906
+ ).action(async (id, opts) => {
1907
+ try {
1908
+ if (opts.approve === opts.reject) {
1909
+ throw new Error("specify exactly one of --approve or --reject");
1910
+ }
1911
+ const res = await new ErdoClient().decideApproval(id, {
1912
+ decision: opts.approve ? "approved" : "rejected",
1913
+ scope: opts.scope
1914
+ });
1915
+ console.log(res.message);
1916
+ } catch (e) {
1917
+ fail(e);
1918
+ }
1919
+ });
1920
+ var attnCmd = program.command("attention").description("The attention feed \u2014 digests, choices, escalations awaiting a human");
1921
+ attnCmd.command("list").description("List attention items").option("--status <status...>", "open | answered | dismissed | expired").option("--open", "shorthand for --status open").option("--engine-actions", "only engine-generated items").option("-n, --limit <n>", "max items", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).action(
1922
+ async (opts) => {
1923
+ try {
1924
+ const statuses = opts.status?.length ? opts.status : opts.open ? ["open"] : void 0;
1925
+ print(
1926
+ await new ErdoClient().listAttentionItems({
1927
+ statuses,
1928
+ engine_actions_only: opts.engineActions,
1929
+ limit: opts.limit,
1930
+ offset: opts.offset
1931
+ })
1932
+ );
1933
+ } catch (e) {
1934
+ fail(e);
1935
+ }
1936
+ }
1937
+ );
1938
+ attnCmd.command("respond <id>").description(
1939
+ `Respond to an attention item. A choice answer's shape is {"<question_id>": ["<option>", ...]}. Answering a choice mapped to experiment variants records your pick as a human comparison in the calibration ledger.`
1940
+ ).option("--answer <json>", `a JSON answer, e.g. '{"q1":["option-a"]}'`).option("--ack", "mark read / acknowledge").option("--dismiss", "dismiss the item").action(async (id, opts) => {
1941
+ try {
1942
+ const given = [opts.answer !== void 0, !!opts.ack, !!opts.dismiss].filter(Boolean).length;
1943
+ if (given !== 1) {
1944
+ fail(new Error("provide exactly one of --answer, --ack, or --dismiss"));
1945
+ }
1946
+ let input;
1947
+ if (opts.answer !== void 0) {
1948
+ let parsed;
1949
+ try {
1950
+ parsed = JSON.parse(opts.answer);
1951
+ } catch {
1952
+ throw new Error(
1953
+ `--answer must be valid JSON, e.g. '{"<question_id>": ["<option>", ...]}'`
1954
+ );
1955
+ }
1956
+ input = { action: "answer", answer: parsed };
1957
+ } else if (opts.ack) {
1958
+ input = { action: "acknowledge" };
1959
+ } else {
1960
+ input = { action: "dismiss" };
1961
+ }
1962
+ print(await new ErdoClient().respondAttentionItem(id, input));
1963
+ } catch (e) {
1964
+ fail(e);
1965
+ }
1966
+ });
964
1967
  var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
965
1968
  function readMaybeFile(v) {
966
1969
  if (!v) return void 0;
@@ -1066,6 +2069,172 @@ datasetsCmd.command("query <slug> <question>").description("Ask a natural-langua
1066
2069
  fail(e);
1067
2070
  }
1068
2071
  });
2072
+ var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
2073
+ datasetsCmd.command("upload <file>").description("Upload a file (CSV, Excel, JSON, ...) and create a dataset from it").option("-n, --name <name>", "display name for the dataset (defaults to the filename)").option("-d, --description <text>", "description shown to agents analyzing the dataset").action(async (file, opts) => {
2074
+ try {
2075
+ const content = readFileSync2(file);
2076
+ if (content.byteLength > MAX_UPLOAD_BYTES) {
2077
+ fail(
2078
+ new Error(
2079
+ `${file} is ${Math.round(content.byteLength / (1024 * 1024))} MB \u2014 the upload limit is 20 MB; upload larger files through the Erdo web app`
2080
+ )
2081
+ );
2082
+ }
2083
+ const filename = basename(file);
2084
+ const res = await new ErdoClient().uploadDatasetFile({
2085
+ filename,
2086
+ content_base64: content.toString("base64"),
2087
+ name: opts.name,
2088
+ description: opts.description
2089
+ });
2090
+ console.log(`Created dataset ${res.slug} (${res.dataset_id})`);
2091
+ if (res.ready === false) {
2092
+ console.log(`Warning: ${res.warning ?? "the file's schema could not be extracted; it is not yet queryable."}`);
2093
+ } else {
2094
+ console.log(`Query it with: erdo datasets query ${res.slug} "..."`);
2095
+ }
2096
+ } catch (e) {
2097
+ fail(e);
2098
+ }
2099
+ });
2100
+ datasetsCmd.command("from-integration <app>").description("Create a dataset backed by a connected database integration (live-queryable, no copy)").requiredOption("-n, --name <name>", "display name for the dataset").option("-s, --schemas <csv>", "schema names to include (see: erdo integrations tables <app>)").option("-d, --description <text>", "description shown to agents analyzing the dataset").action(async (app, opts) => {
2101
+ try {
2102
+ const res = await new ErdoClient().createIntegrationDataset({
2103
+ integration: app,
2104
+ name: opts.name,
2105
+ description: opts.description,
2106
+ schemas: opts.schemas ? opts.schemas.split(",").map((s) => s.trim()) : void 0
2107
+ });
2108
+ console.log(`Created dataset ${res.slug} (${res.dataset_id}), status: ${res.status}`);
2109
+ console.log(`Table discovery runs in the background \u2014 query shortly with: erdo datasets query ${res.slug} "..."`);
2110
+ } catch (e) {
2111
+ fail(e);
2112
+ }
2113
+ });
2114
+ var OPERATORS = ["equals", "not_equals", "greater_than", "less_than", "contains", "between"];
2115
+ function parseCondition(s) {
2116
+ const m = s.trim().match(/^(\S+)\s+(\S+)\s+([\s\S]+)$/);
2117
+ if (!m) {
2118
+ fail(new Error(`invalid --where ${JSON.stringify(s)}; expected '<column> <operator> <value>', e.g. 'email contains @example.com'`));
2119
+ }
2120
+ const [, column, operator, value] = m;
2121
+ if (!OPERATORS.includes(operator)) {
2122
+ fail(new Error(`unsupported operator ${JSON.stringify(operator)}; allowed: ${OPERATORS.join(", ")}`));
2123
+ }
2124
+ return { column, operator, value };
2125
+ }
2126
+ var filterCmd = datasetsCmd.command("filter").description("Manage a dataset's default filters");
2127
+ filterCmd.command("list <slug>").description("List the default filters on a dataset").action(async (slug) => {
2128
+ try {
2129
+ print(await new ErdoClient().listDatasetFilters(slug));
2130
+ } catch (e) {
2131
+ fail(e);
2132
+ }
2133
+ });
2134
+ var integrationsCmd = program.command("integrations").description("Connect and inspect integrations");
2135
+ integrationsCmd.command("list").description("List connected integrations").action(async () => {
2136
+ try {
2137
+ const { integrations } = await new ErdoClient().listIntegrations();
2138
+ for (const i of integrations) console.log(`${i.app} ${i.status} ${i.auth_type} ${i.name}`);
2139
+ } catch (e) {
2140
+ fail(e);
2141
+ }
2142
+ });
2143
+ integrationsCmd.command("apps [query]").description("Search connectable apps (native integrations + SaaS apps)").action(async (query) => {
2144
+ try {
2145
+ const { apps } = await new ErdoClient().searchIntegrationApps(query);
2146
+ for (const a of apps) console.log(`${a.app} ${a.source} ${a.auth_types.join(",")} ${a.name}`);
2147
+ } catch (e) {
2148
+ fail(e);
2149
+ }
2150
+ });
2151
+ integrationsCmd.command("connect <app>").description("Connect an app \u2014 pass credentials with -c, or get a browser connect URL for OAuth apps").option("-c, --credential <key=value...>", "credential field (repeatable)", (v, acc) => acc.concat(v), []).option("-n, --name <name>", "display name for the connection").action(async (app, opts) => {
2152
+ try {
2153
+ let credentials;
2154
+ if (opts.credential.length) {
2155
+ credentials = {};
2156
+ for (const c of opts.credential) {
2157
+ const eq = c.indexOf("=");
2158
+ if (eq < 1) fail(new Error(`invalid --credential ${JSON.stringify(c)}; expected key=value`));
2159
+ credentials[c.slice(0, eq)] = c.slice(eq + 1);
2160
+ }
2161
+ }
2162
+ const res = await new ErdoClient().connectIntegration({ app, name: opts.name, credentials });
2163
+ print(res);
2164
+ if (res.connect_url) {
2165
+ console.error(`
2166
+ Open this URL in a browser to authorize, then run: erdo integrations status ${app}`);
2167
+ }
2168
+ } catch (e) {
2169
+ fail(e);
2170
+ }
2171
+ });
2172
+ integrationsCmd.command("status <app>").description("Check whether an app is connected (completes browser-authorized connections)").action(async (app) => {
2173
+ try {
2174
+ print(await new ErdoClient().checkIntegrationConnection(app));
2175
+ } catch (e) {
2176
+ fail(e);
2177
+ }
2178
+ });
2179
+ integrationsCmd.command("tables <app> [schema]").description("List a connected database integration's schemas, or a schema's tables with columns").action(async (app, schema) => {
2180
+ try {
2181
+ const res = await new ErdoClient().discoverIntegrationTables(app, schema);
2182
+ if (!schema) {
2183
+ for (const s of res.schemas) console.log(`${s.name} ${s.type}`);
2184
+ if (res.schemas.length) console.log(`
2185
+ List tables with: erdo integrations tables ${app} <schema>`);
2186
+ return;
2187
+ }
2188
+ for (const t of res.tables ?? []) {
2189
+ const rows = t.estimated_row_count != null ? ` ~${t.estimated_row_count} rows` : "";
2190
+ console.log(`${t.schema_name}.${t.table_name} ${t.columns.length} columns${rows}`);
2191
+ }
2192
+ } catch (e) {
2193
+ fail(e);
2194
+ }
2195
+ });
2196
+ var pipelinesCmd = program.command("event-pipelines").description("Inspect event pipelines (lead-capture / webhook flows)");
2197
+ pipelinesCmd.command("list").description("List event pipelines in the active org").option("-n, --limit <n>", "max rows", (v) => parseInt(v, 10)).action(async (opts) => {
2198
+ try {
2199
+ print(await new ErdoClient().listEventPipelines(opts.limit));
2200
+ } catch (e) {
2201
+ fail(e);
2202
+ }
2203
+ });
2204
+ pipelinesCmd.command("get <slug>").description("Show one event pipeline \u2014 state, source, endpoint URL, actions, write targets").action(async (slug) => {
2205
+ try {
2206
+ print(await new ErdoClient().getEventPipeline(slug));
2207
+ } catch (e) {
2208
+ fail(e);
2209
+ }
2210
+ });
2211
+ filterCmd.command("add <slug>").description("Add a default filter to a dataset").requiredOption(
2212
+ "--where <condition>",
2213
+ "a '<column> <operator> <value>' condition (repeatable, AND-combined), e.g. --where 'email contains @example.com'. Operators: " + OPERATORS.join(", ") + " (for 'between' use 'col between 10,100')",
2214
+ collect,
2215
+ []
2216
+ ).option("--name <name>", "optional human-readable label, e.g. 'Exclude test submissions'").action(async (slug, opts) => {
2217
+ try {
2218
+ const conditions = opts.where.map(parseCondition);
2219
+ print(await new ErdoClient().createDatasetFilter(slug, { name: opts.name, conditions }));
2220
+ } catch (e) {
2221
+ fail(e);
2222
+ }
2223
+ });
2224
+ filterCmd.command("remove <slug> <filterId>").description("Remove a filter from a dataset (use 'filter list' to find the id). Hides nothing further; never deletes data.").action(async (slug, filterId) => {
2225
+ try {
2226
+ print(await new ErdoClient().deleteDatasetFilter(slug, filterId));
2227
+ } catch (e) {
2228
+ fail(e);
2229
+ }
2230
+ });
2231
+ pipelinesCmd.command("executions <slug>").description("Show an event pipeline's recent run history (newest first)").option("-n, --limit <n>", "max executions", (v) => parseInt(v, 10)).action(async (slug, opts) => {
2232
+ try {
2233
+ print(await new ErdoClient().listEventPipelineExecutions(slug, opts.limit));
2234
+ } catch (e) {
2235
+ fail(e);
2236
+ }
2237
+ });
1069
2238
  var knowledgeCmd = program.command("knowledge").description("Knowledge objects");
1070
2239
  knowledgeCmd.command("list").description("List knowledge objects").action(async () => {
1071
2240
  try {
@@ -1081,6 +2250,15 @@ knowledgeCmd.command("search <query>").description("Semantic search over knowled
1081
2250
  fail(e);
1082
2251
  }
1083
2252
  });
2253
+ knowledgeCmd.command("visibility <id> <visibility>").description(
2254
+ "Set a knowledge object's visibility: 'workspace' (organization-internal) or 'public' (answerable by external surfaces like the website voice widget once approved)"
2255
+ ).action(async (id, visibility) => {
2256
+ try {
2257
+ print(await new ErdoClient().setKnowledgeVisibility(id, visibility));
2258
+ } catch (e) {
2259
+ fail(e);
2260
+ }
2261
+ });
1084
2262
  var autoCmd = program.command("automations").description("Scheduled automations (heartbeats)");
1085
2263
  autoCmd.command("list").description("List automations").action(async () => {
1086
2264
  try {