@erdoai/cli 0.5.1 → 0.23.1

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 +1245 -65
  2. package/package.json +2 -2
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(
@@ -268,12 +490,83 @@ var ErdoClient = class {
268
490
  "/v1/datasets"
269
491
  );
270
492
  }
271
- queryDataset(slug, query, timezone) {
493
+ // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
494
+ // `query` made every `erdo datasets query` fail with "question is required".
495
+ queryDataset(slug, question, timezone) {
272
496
  return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/query-nl`, {
273
- query,
497
+ question,
274
498
  timezone
275
499
  });
276
500
  }
501
+ uploadDatasetFile(body) {
502
+ return this.request("POST", "/v1/datasets-upload", body);
503
+ }
504
+ createIntegrationDataset(body) {
505
+ return this.request(
506
+ "POST",
507
+ "/v1/integration-datasets",
508
+ body
509
+ );
510
+ }
511
+ // --- integrations ---
512
+ discoverIntegrationTables(integration, schemaName) {
513
+ const qs = schemaName ? `?schema_name=${encodeURIComponent(schemaName)}` : "";
514
+ return this.request("GET", `/v1/integrations/${encodeURIComponent(integration)}/tables${qs}`);
515
+ }
516
+ listIntegrations() {
517
+ return this.request("GET", "/v1/integrations");
518
+ }
519
+ searchIntegrationApps(query) {
520
+ const qs = query ? `?query=${encodeURIComponent(query)}` : "";
521
+ return this.request("GET", `/v1/integration-apps${qs}`);
522
+ }
523
+ connectIntegration(body) {
524
+ return this.request("POST", "/v1/integrations-connect", body);
525
+ }
526
+ checkIntegrationConnection(app) {
527
+ return this.request("GET", `/v1/integrations-connect/${encodeURIComponent(app)}`);
528
+ }
529
+ // --- dataset filters ---
530
+ listDatasetFilters(datasetSlug) {
531
+ return this.request(
532
+ "GET",
533
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/filters`
534
+ );
535
+ }
536
+ createDatasetFilter(datasetSlug, body) {
537
+ return this.request(
538
+ "POST",
539
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/filters`,
540
+ body
541
+ );
542
+ }
543
+ deleteDatasetFilter(datasetSlug, filterId) {
544
+ return this.request(
545
+ "DELETE",
546
+ `/v1/datasets/${encodeURIComponent(datasetSlug)}/filters/${encodeURIComponent(filterId)}`
547
+ );
548
+ }
549
+ // --- event pipelines ---
550
+ listEventPipelines(limit, offset) {
551
+ const q = new URLSearchParams();
552
+ if (limit) q.set("limit", String(limit));
553
+ if (offset) q.set("offset", String(offset));
554
+ const qs = q.toString();
555
+ return this.request("GET", `/v1/event-pipelines${qs ? `?${qs}` : ""}`);
556
+ }
557
+ getEventPipeline(slug) {
558
+ return this.request("GET", `/v1/event-pipelines/${encodeURIComponent(slug)}`);
559
+ }
560
+ listEventPipelineExecutions(slug, limit, offset) {
561
+ const q = new URLSearchParams();
562
+ if (limit) q.set("limit", String(limit));
563
+ if (offset) q.set("offset", String(offset));
564
+ const qs = q.toString();
565
+ return this.request(
566
+ "GET",
567
+ `/v1/event-pipelines/${encodeURIComponent(slug)}/executions${qs ? `?${qs}` : ""}`
568
+ );
569
+ }
277
570
  // --- knowledge ---
278
571
  listKnowledge() {
279
572
  return this.request("GET", "/v1/knowledge");
@@ -283,6 +576,9 @@ var ErdoClient = class {
283
576
  if (limit) params.set("limit", String(limit));
284
577
  return this.request("GET", `/v1/knowledge-search?${params.toString()}`);
285
578
  }
579
+ setKnowledgeVisibility(id, visibility) {
580
+ return this.request("PATCH", `/v1/knowledge/${id}/visibility`, { visibility });
581
+ }
286
582
  // --- automations (heartbeats) ---
287
583
  listHeartbeats() {
288
584
  return this.request(
@@ -331,6 +627,24 @@ var ErdoClient = class {
331
627
  getAgentRun(id) {
332
628
  return this.request("GET", `/v1/runs/${encodeURIComponent(id)}`);
333
629
  }
630
+ // --- approvals ---
631
+ listApprovals(opts) {
632
+ const params = new URLSearchParams();
633
+ if (opts.status) params.set("status", opts.status);
634
+ if (opts.limit) params.set("limit", String(opts.limit));
635
+ const qs = params.toString();
636
+ return this.request(
637
+ "GET",
638
+ `/v1/approvals${qs ? `?${qs}` : ""}`
639
+ );
640
+ }
641
+ decideApproval(id, input) {
642
+ return this.request(
643
+ "POST",
644
+ `/v1/approvals/${encodeURIComponent(id)}/decide`,
645
+ input
646
+ );
647
+ }
334
648
  // --- pages / artifacts ---
335
649
  deployPage(input) {
336
650
  return this.request("POST", "/v1/pages", input);
@@ -365,9 +679,10 @@ import { spawn } from "child_process";
365
679
  import { randomBytes } from "crypto";
366
680
  import { createServer } from "http";
367
681
  import { hostname } from "os";
682
+ import * as readline from "readline";
368
683
  var REDIRECT_URI = "http://localhost:8080/callback";
369
684
  var PORT = 8080;
370
- var LOGIN_TIMEOUT_MS = 3 * 60 * 1e3;
685
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
371
686
  function openBrowser(url) {
372
687
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
373
688
  const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
@@ -376,9 +691,56 @@ function openBrowser(url) {
376
691
  } catch {
377
692
  }
378
693
  }
694
+ function copyToClipboard(text) {
695
+ const b64 = Buffer.from(text, "utf8").toString("base64");
696
+ let osc = `\x1B]52;c;${b64}\x07`;
697
+ if (process.env.TMUX) osc = `\x1BPtmux;${osc.replace(/\x1b/g, "\x1B\x1B")}\x1B\\`;
698
+ process.stdout.write(osc);
699
+ const cmd = process.platform === "darwin" ? "pbcopy" : process.platform === "win32" ? "clip" : "xclip";
700
+ const args = process.platform === "linux" ? ["-selection", "clipboard"] : [];
701
+ try {
702
+ const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
703
+ child.on("error", () => {
704
+ });
705
+ child.stdin.end(text);
706
+ child.unref();
707
+ } catch {
708
+ }
709
+ }
710
+ function parsePastedCode(input, state) {
711
+ const text = input.trim();
712
+ if (!text) return null;
713
+ if (text.includes("code=")) {
714
+ let params;
715
+ try {
716
+ params = new URL(text).searchParams;
717
+ } catch {
718
+ params = new URLSearchParams(text.replace(/^[^?]*\?/, ""));
719
+ }
720
+ const code = params.get("code");
721
+ if (!code) return { error: "that looks like a callback URL but has no code parameter" };
722
+ const gotState = params.get("state");
723
+ if (!gotState || gotState !== state) return { error: "state mismatch \u2014 copy the URL from THIS login attempt's tab" };
724
+ return { code };
725
+ }
726
+ if (/^[A-Za-z0-9_-]{20,}$/.test(text)) return { code: text };
727
+ return { error: "couldn't find an authorization code in that \u2014 paste the full localhost callback URL" };
728
+ }
379
729
  function waitForCode(state) {
380
730
  return new Promise((resolve, reject) => {
381
- const server = createServer((req, res) => {
731
+ let settled = false;
732
+ let server;
733
+ let rl;
734
+ const finish = (fn) => {
735
+ if (settled) return;
736
+ settled = true;
737
+ server?.close();
738
+ if (rl) rl.close();
739
+ if (process.stdin.isTTY) process.stdin.setRawMode?.(false);
740
+ process.stdin.pause();
741
+ fn();
742
+ };
743
+ server = createServer((req, res) => {
382
744
  const u = new URL(req.url || "", REDIRECT_URI);
383
745
  if (u.pathname !== "/callback") {
384
746
  res.writeHead(404);
@@ -391,16 +753,17 @@ function waitForCode(state) {
391
753
  res.end(
392
754
  '<html><head><meta charset="utf-8"></head><body><h2>Erdo CLI \u2014 authenticated. You can close this tab.</h2></body></html>'
393
755
  );
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);
756
+ if (gotState !== state) return finish(() => reject(new Error("state mismatch \u2014 login aborted")));
757
+ if (!code) return finish(() => reject(new Error("no authorization code returned")));
758
+ finish(() => resolve(code));
398
759
  });
399
760
  server.on("error", (err) => {
400
761
  if (err.code === "EADDRINUSE") {
401
- reject(new Error(`port ${PORT} is in use \u2014 free it and retry (or use 'erdo login --key')`));
762
+ finish(
763
+ () => reject(new Error(`port ${PORT} is in use \u2014 free it and retry (or use 'erdo login --key')`))
764
+ );
402
765
  } else {
403
- reject(err);
766
+ finish(() => reject(err));
404
767
  }
405
768
  });
406
769
  server.listen(PORT, () => {
@@ -409,12 +772,58 @@ function waitForCode(state) {
409
772
  const authURL = `${api}/auth/cli?response_type=code&client_id=erdo-cli&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&state=${state}&token_name=${encodeURIComponent(tokenName)}`;
410
773
  console.log(`Opening your browser to authorize\u2026
411
774
  If it doesn't open, visit:
412
- ${authURL}`);
775
+ ${authURL}
776
+ `);
777
+ if (process.stdin.isTTY) {
778
+ console.log(
779
+ "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."
780
+ );
781
+ }
413
782
  openBrowser(authURL);
783
+ if (process.stdin.isTTY) {
784
+ readline.emitKeypressEvents(process.stdin);
785
+ process.stdin.setRawMode?.(true);
786
+ process.stdin.resume();
787
+ let buffer = "";
788
+ process.stdin.on("keypress", (str, key) => {
789
+ if (settled) return;
790
+ if (key?.ctrl && key.name === "c") {
791
+ finish(() => reject(new Error("login cancelled")));
792
+ return;
793
+ }
794
+ if (key?.name === "return" || key?.name === "enter") {
795
+ const parsed = parsePastedCode(buffer, state);
796
+ buffer = "";
797
+ process.stdout.write("\n");
798
+ if (!parsed) return;
799
+ if ("error" in parsed) {
800
+ console.log(` \u2717 ${parsed.error}`);
801
+ return;
802
+ }
803
+ finish(() => resolve(parsed.code));
804
+ return;
805
+ }
806
+ if (key?.name === "backspace") {
807
+ if (buffer.length > 0) {
808
+ buffer = buffer.slice(0, -1);
809
+ process.stdout.write("\b \b");
810
+ }
811
+ return;
812
+ }
813
+ if ((str === "c" || str === "C") && buffer.length === 0) {
814
+ copyToClipboard(authURL);
815
+ console.log(" \u2713 copied the login URL to your clipboard");
816
+ return;
817
+ }
818
+ if (str && !key?.ctrl && !key?.meta) {
819
+ buffer += str;
820
+ process.stdout.write(str);
821
+ }
822
+ });
823
+ }
414
824
  });
415
825
  setTimeout(() => {
416
- server.close();
417
- reject(new Error("login timed out after 3 minutes"));
826
+ finish(() => reject(new Error("login timed out after 5 minutes")));
418
827
  }, LOGIN_TIMEOUT_MS).unref();
419
828
  });
420
829
  }
@@ -508,10 +917,29 @@ function fail(err) {
508
917
  }
509
918
  process.exit(1);
510
919
  }
920
+ function awaitingApprovalMessage(threadID) {
921
+ return [
922
+ "the run paused for approval.",
923
+ " erdo approvals list --status pending",
924
+ " erdo approvals decide <id> --approve",
925
+ ` erdo agent messages ${threadID}`
926
+ ].join("\n");
927
+ }
928
+ function timedOutMessage(threadID) {
929
+ return [
930
+ "timed out waiting for the run to finish \u2014 it is still running on its thread.",
931
+ ` erdo agent messages ${threadID}`,
932
+ ` erdo runs list --thread ${threadID}`
933
+ ].join("\n");
934
+ }
511
935
  function print(value) {
512
936
  console.log(JSON.stringify(value, null, 2));
513
937
  }
514
- var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
938
+ var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
939
+ var collect = (v, acc) => {
940
+ acc.push(v);
941
+ return acc;
942
+ };
515
943
  function summariseResults(results, cases = []) {
516
944
  const nameByID = new Map(cases.map((c) => [c.id, c.name]));
517
945
  for (const r of results) {
@@ -669,6 +1097,20 @@ org.command("use [idOrSlug]").description("Set the active org for the current ac
669
1097
  fail(e);
670
1098
  }
671
1099
  });
1100
+ org.command("autonomy [mode]").description("Show or set the org's engine-autonomy mode (autopilot | propose | strict)").action(async (mode) => {
1101
+ try {
1102
+ const api = new ErdoClient();
1103
+ if (mode) {
1104
+ const { mode: updated } = await api.setAutonomyMode(mode);
1105
+ console.log(`Autonomy mode set to: ${updated}`);
1106
+ } else {
1107
+ const { mode: current } = await api.getAutonomyMode();
1108
+ console.log(`Autonomy mode: ${current}`);
1109
+ }
1110
+ } catch (e) {
1111
+ fail(e);
1112
+ }
1113
+ });
672
1114
  var evalCmd = program.command("eval").description("Run and manage evals");
673
1115
  evalCmd.command("suites").description("List eval suites in the active org (+ global)").action(async () => {
674
1116
  try {
@@ -687,6 +1129,475 @@ evalCmd.command("suite <slug>").description("Show a suite and its cases").action
687
1129
  fail(e);
688
1130
  }
689
1131
  });
1132
+ var projectCmd = program.command("project").description("Projects \u2014 the container workstreams and experiments live in");
1133
+ 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) => {
1134
+ try {
1135
+ print(await new ErdoClient().listProjects({ limit: opts.limit, offset: opts.offset }));
1136
+ } catch (e) {
1137
+ fail(e);
1138
+ }
1139
+ });
1140
+ 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) => {
1141
+ try {
1142
+ print(
1143
+ await new ErdoClient().createProject({
1144
+ slug: opts.slug,
1145
+ name: opts.name,
1146
+ description: opts.description
1147
+ })
1148
+ );
1149
+ } catch (e) {
1150
+ fail(e);
1151
+ }
1152
+ });
1153
+ var wsCmd = program.command("workstream").description("Drive and observe Workstreams");
1154
+ 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) => {
1155
+ try {
1156
+ print(await new ErdoClient().listWorkstreams(opts.status));
1157
+ } catch (e) {
1158
+ fail(e);
1159
+ }
1160
+ });
1161
+ wsCmd.command("get <slug>").description("Show a workstream \u2014 status, phases, resources, recent log").action(async (slug) => {
1162
+ try {
1163
+ print(await new ErdoClient().getWorkstream(slug));
1164
+ } catch (e) {
1165
+ fail(e);
1166
+ }
1167
+ });
1168
+ 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) => {
1169
+ try {
1170
+ print(
1171
+ await new ErdoClient().createWorkstream({
1172
+ project_slug: opts.project,
1173
+ slug: opts.slug,
1174
+ title: opts.title,
1175
+ description: opts.description
1176
+ })
1177
+ );
1178
+ } catch (e) {
1179
+ fail(e);
1180
+ }
1181
+ });
1182
+ wsCmd.command("log <slug> <body...>").description("Append a line to the workstream's event log").action(async (slug, body) => {
1183
+ try {
1184
+ print(await new ErdoClient().appendWorkstreamEvent(slug, body.join(" ")));
1185
+ } catch (e) {
1186
+ fail(e);
1187
+ }
1188
+ });
1189
+ 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) => {
1190
+ try {
1191
+ print(await new ErdoClient().listWorkstreamEvents(slug, opts.limit));
1192
+ } catch (e) {
1193
+ fail(e);
1194
+ }
1195
+ });
1196
+ 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(
1197
+ async (slug, opts) => {
1198
+ try {
1199
+ print(
1200
+ await new ErdoClient().addWorkstreamPhase(slug, {
1201
+ slug: opts.phaseSlug,
1202
+ title: opts.title,
1203
+ skill_slug: opts.skill
1204
+ })
1205
+ );
1206
+ } catch (e) {
1207
+ fail(e);
1208
+ }
1209
+ }
1210
+ );
1211
+ 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(
1212
+ async (slug, opts) => {
1213
+ try {
1214
+ print(
1215
+ await new ErdoClient().updateWorkState(slug, {
1216
+ status: opts.status,
1217
+ title: opts.title,
1218
+ description: opts.description,
1219
+ summary: opts.summary
1220
+ })
1221
+ );
1222
+ } catch (e) {
1223
+ fail(e);
1224
+ }
1225
+ }
1226
+ );
1227
+ 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(
1228
+ async (slug, opts) => {
1229
+ try {
1230
+ const client = new ErdoClient();
1231
+ if (opts.tokenBudget !== void 0 || opts.escalationBudget !== void 0) {
1232
+ await client.updateWorkState(slug, {
1233
+ token_budget_millicents: opts.tokenBudget,
1234
+ escalation_budget_per_day: opts.escalationBudget
1235
+ });
1236
+ }
1237
+ print(
1238
+ await client.armWorkstreamLoop(slug, {
1239
+ interval_minutes: opts.interval,
1240
+ model: opts.model,
1241
+ timezone: opts.timezone
1242
+ })
1243
+ );
1244
+ } catch (e) {
1245
+ fail(e);
1246
+ }
1247
+ }
1248
+ );
1249
+ wsCmd.command("ledger <slug>").description(
1250
+ "Read the allocator's single ledger view \u2014 budget, experiments + observations, judge calibration, attention items, allocator recommendation"
1251
+ ).option("--observations <n>", "observations to include per experiment", (v) => parseInt(v, 10)).action(async (slug, opts) => {
1252
+ try {
1253
+ print(await new ErdoClient().readWorkstreamLedger(slug, opts.observations));
1254
+ } catch (e) {
1255
+ fail(e);
1256
+ }
1257
+ });
1258
+ 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(
1259
+ async (slug, opts) => {
1260
+ try {
1261
+ print(
1262
+ await new ErdoClient().attachWorkstreamResource(slug, {
1263
+ resource_type: opts.type,
1264
+ resource_id: opts.id,
1265
+ relationship_type: opts.rel,
1266
+ title: opts.title,
1267
+ phase_id: opts.phase
1268
+ })
1269
+ );
1270
+ } catch (e) {
1271
+ fail(e);
1272
+ }
1273
+ }
1274
+ );
1275
+ function parseVariant(spec) {
1276
+ const v = {};
1277
+ for (const raw of spec.split(",")) {
1278
+ const part = raw.trim();
1279
+ if (!part) continue;
1280
+ const eq = part.indexOf("=");
1281
+ if (eq === -1) {
1282
+ if (part === "control" || part === "is_control") {
1283
+ v.is_control = true;
1284
+ continue;
1285
+ }
1286
+ throw new Error(`--variant: unknown flag "${part}" (expected key=, label=, page=, alloc=, class=, or 'control')`);
1287
+ }
1288
+ const k = part.slice(0, eq).trim();
1289
+ const val = part.slice(eq + 1).trim();
1290
+ switch (k) {
1291
+ case "key":
1292
+ v.key = val;
1293
+ break;
1294
+ case "label":
1295
+ v.label = val;
1296
+ break;
1297
+ case "alloc":
1298
+ case "allocation_percent":
1299
+ v.allocation_percent = parseInt(val, 10);
1300
+ if (isNaN(v.allocation_percent)) throw new Error(`--variant alloc= must be an integer, got: ${val}`);
1301
+ break;
1302
+ case "control":
1303
+ case "is_control":
1304
+ v.is_control = val !== "false";
1305
+ break;
1306
+ case "page":
1307
+ v.treatment_resource_type = "page";
1308
+ v.treatment_resource_id = val;
1309
+ break;
1310
+ case "type":
1311
+ case "treatment_resource_type":
1312
+ v.treatment_resource_type = val;
1313
+ break;
1314
+ case "resource":
1315
+ case "treatment_resource_id":
1316
+ v.treatment_resource_id = val;
1317
+ break;
1318
+ case "class":
1319
+ case "treatment_class":
1320
+ v.treatment_class = val;
1321
+ break;
1322
+ default:
1323
+ throw new Error(`--variant: unknown key "${k}"`);
1324
+ }
1325
+ }
1326
+ if (!v.key) {
1327
+ throw new Error('--variant requires key=, e.g. --variant "key=control,label=Current,control"');
1328
+ }
1329
+ if (!v.label) v.label = v.key;
1330
+ return v;
1331
+ }
1332
+ function parseEvidence(spec) {
1333
+ const s = spec.trim();
1334
+ if (!s.includes("=")) {
1335
+ const idx = s.lastIndexOf(":");
1336
+ const slug2 = idx === -1 ? s : s.slice(0, idx);
1337
+ const role2 = idx === -1 ? "primary" : s.slice(idx + 1);
1338
+ if (!slug2.trim()) throw new Error("--evidence requires a dataset slug");
1339
+ return { dataset_slug: slug2.trim(), role: (role2 || "primary").trim() };
1340
+ }
1341
+ let slug = "";
1342
+ let role = "primary";
1343
+ for (const raw of s.split(",")) {
1344
+ const part = raw.trim();
1345
+ if (!part) continue;
1346
+ const eq = part.indexOf("=");
1347
+ if (eq === -1) throw new Error(`--evidence: expected key=value pair, got "${part}"`);
1348
+ const k = part.slice(0, eq).trim();
1349
+ const val = part.slice(eq + 1).trim();
1350
+ if (k === "slug" || k === "dataset" || k === "dataset_slug") slug = val;
1351
+ else if (k === "role") role = val;
1352
+ else throw new Error(`--evidence: unknown key "${k}"`);
1353
+ }
1354
+ if (!slug) throw new Error('--evidence requires slug=, e.g. --evidence "slug=my-leads,role=primary"');
1355
+ return { dataset_slug: slug, role };
1356
+ }
1357
+ var expCmd = program.command("experiment").description("Drive and observe Experiments");
1358
+ 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) => {
1359
+ try {
1360
+ print(
1361
+ await new ErdoClient().listExperiments({
1362
+ workstream_slug: opts.workstream,
1363
+ scope: opts.scope,
1364
+ status: opts.status
1365
+ })
1366
+ );
1367
+ } catch (e) {
1368
+ fail(e);
1369
+ }
1370
+ });
1371
+ expCmd.command("get <slug>").description("Show an experiment \u2014 hypothesis, variants, evidence, status, decision").action(async (slug) => {
1372
+ try {
1373
+ print(await new ErdoClient().getExperiment(slug));
1374
+ } catch (e) {
1375
+ fail(e);
1376
+ }
1377
+ });
1378
+ expCmd.command("policy <slug>").description(
1379
+ "Show the decision policy state \u2014 per-variant posteriors, allocation, win frequencies, stop probabilities, calibration gate, recommended decision, and HAIPW effect readout"
1380
+ ).action(async (slug) => {
1381
+ try {
1382
+ print(await new ErdoClient().getExperimentPolicy(slug));
1383
+ } catch (e) {
1384
+ fail(e);
1385
+ }
1386
+ });
1387
+ expCmd.command("create").description("Create an experiment with its variants, evidence datasets and decision rule").requiredOption("--project <slug>", "project slug").requiredOption("--slug <slug>", "experiment slug (unique per org)").requiredOption("--title <title>", "title").option("--workstream <slug>", "host workstream").option("--scope <scope>", "local (default), platform_canary, platform_global").option("--hypothesis <text>", "hypothesis markdown").option("--primary-metric <metric>", "the metric the decision rule reads").option("--decision-rule <text>", "the rule that decides ship/stop from the primary metric").option("--guardrail <metric>", "a metric that must not regress (repeatable)", collect, []).option(
1388
+ "--variant <spec>",
1389
+ 'a variant "key=b,label=3-field form,page=<artifact-id>,alloc=50,class=form_length_change,control" (repeatable). Fields: key=, label=, page=, alloc=, class=, and the bare flag control (or control=true) to mark the baseline. class= declares the structural change vs control for cross-experiment learning: headline_change | hero_media_change | cta_change | form_length_change | social_proof_add | layout_reorder | offer_change | full_page_rebuild',
1390
+ collect,
1391
+ []
1392
+ ).option(
1393
+ "--evidence <spec>",
1394
+ 'a measurement dataset "slug=<dataset>,role=primary" or "<dataset-slug>:<role>" (repeatable)',
1395
+ collect,
1396
+ []
1397
+ ).action(
1398
+ async (opts) => {
1399
+ try {
1400
+ const variants = opts.variant.map(parseVariant);
1401
+ const evidence_datasets = opts.evidence.map(parseEvidence);
1402
+ print(
1403
+ await new ErdoClient().createExperiment({
1404
+ project_slug: opts.project,
1405
+ slug: opts.slug,
1406
+ title: opts.title,
1407
+ workstream_slug: opts.workstream,
1408
+ scope: opts.scope,
1409
+ hypothesis_markdown: opts.hypothesis,
1410
+ primary_metric: opts.primaryMetric,
1411
+ decision_rule_markdown: opts.decisionRule,
1412
+ guardrail_metrics: opts.guardrail.length ? opts.guardrail : void 0,
1413
+ variants: variants.length ? variants : void 0,
1414
+ evidence_datasets: evidence_datasets.length ? evidence_datasets : void 0
1415
+ })
1416
+ );
1417
+ } catch (e) {
1418
+ fail(e);
1419
+ }
1420
+ }
1421
+ );
1422
+ 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(
1423
+ async (slug, opts) => {
1424
+ try {
1425
+ print(
1426
+ await new ErdoClient().listExperimentObservations(slug, {
1427
+ variant_key: opts.variant,
1428
+ observation_type: opts.type,
1429
+ judge_slug: opts.judge,
1430
+ limit: opts.limit
1431
+ })
1432
+ );
1433
+ } catch (e) {
1434
+ fail(e);
1435
+ }
1436
+ }
1437
+ );
1438
+ expCmd.command("priors").description(
1439
+ "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."
1440
+ ).action(async () => {
1441
+ try {
1442
+ print(await new ErdoClient().listCrossCustomerPriors());
1443
+ } catch (e) {
1444
+ fail(e);
1445
+ }
1446
+ });
1447
+ 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(
1448
+ async (slug, opts) => {
1449
+ try {
1450
+ const client = new ErdoClient();
1451
+ const started = await client.runPersonaPanel(slug, {
1452
+ persona_slugs: opts.persona,
1453
+ variant_keys: opts.variant,
1454
+ draws: opts.draws
1455
+ });
1456
+ print(started);
1457
+ if (!opts.wait) return;
1458
+ const deadline = Date.now() + 20 * 60 * 1e3;
1459
+ process.stderr.write("waiting for the panel ranking");
1460
+ while (Date.now() < deadline) {
1461
+ await new Promise((r) => setTimeout(r, 5e3));
1462
+ process.stderr.write(".");
1463
+ const res = await client.listExperimentObservations(slug, {
1464
+ observation_type: "decision_check",
1465
+ limit: 20
1466
+ });
1467
+ for (const obs of res.observations ?? []) {
1468
+ const details = typeof obs.details === "string" ? JSON.parse(obs.details) : obs.details;
1469
+ if (details && details.panel_run_id === started.panel_run_id) {
1470
+ process.stderr.write("\n");
1471
+ print({ ranking: details.ranking, kill_list: details.kill_list ?? [], primary_metric: details.primary_metric, failed_cells: details.failed_cells ?? [] });
1472
+ return;
1473
+ }
1474
+ }
1475
+ }
1476
+ process.stderr.write("\n");
1477
+ fail(new Error(`panel still running after 20m \u2014 check later with: erdo experiment observations ${slug} --type decision_check`));
1478
+ } catch (e) {
1479
+ fail(e);
1480
+ }
1481
+ }
1482
+ );
1483
+ expCmd.command("diagnose <slug>").description(
1484
+ "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)"
1485
+ ).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(
1486
+ async (slug, opts) => {
1487
+ try {
1488
+ const ack = await new ErdoClient().diagnoseExperimentPersonas(slug, {
1489
+ top_k: opts.topK,
1490
+ survivor_variant_keys: opts.survivor,
1491
+ persona_slugs: opts.persona
1492
+ });
1493
+ print(ack);
1494
+ process.stderr.write(
1495
+ `sessions run in the background; read the narratives with: erdo experiment observations ${slug} --type action_taken
1496
+ `
1497
+ );
1498
+ } catch (e) {
1499
+ fail(e);
1500
+ }
1501
+ }
1502
+ );
1503
+ 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) => {
1504
+ try {
1505
+ print(
1506
+ await new ErdoClient().updateExperiment(slug, {
1507
+ status: "decided",
1508
+ decision: opts.decision,
1509
+ outcome_markdown: opts.outcome
1510
+ })
1511
+ );
1512
+ } catch (e) {
1513
+ fail(e);
1514
+ }
1515
+ });
1516
+ expCmd.command("set-status <slug> <status>").description("Set an experiment's status (planned, running, reading, decided)").action(async (slug, status) => {
1517
+ try {
1518
+ print(await new ErdoClient().updateExperiment(slug, { status }));
1519
+ } catch (e) {
1520
+ fail(e);
1521
+ }
1522
+ });
1523
+ expCmd.command("calibration").description(
1524
+ "Read each judge's track record \u2014 paired predictions and pairwise agreement vs measured reality (the trust dial)"
1525
+ ).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) => {
1526
+ try {
1527
+ const res = await new ErdoClient().judgeCalibration({
1528
+ project_slug: opts.project,
1529
+ limit: opts.limit
1530
+ });
1531
+ if (opts.judge && Array.isArray(res.judges)) {
1532
+ const needle = opts.judge.toLowerCase();
1533
+ res.judges = res.judges.filter((j) => j.judge_slug.toLowerCase().includes(needle));
1534
+ }
1535
+ print(res);
1536
+ } catch (e) {
1537
+ fail(e);
1538
+ }
1539
+ });
1540
+ expCmd.command("record-decision <slug>").description(
1541
+ "Record that you are overriding the allocator's recommendation for one bet (calibration data on the allocator)"
1542
+ ).requiredOption("--variant <key>", "the variant/bet you are deciding on").requiredOption(
1543
+ "--recommended <action>",
1544
+ "what the allocator recommended: fund | hold | kill_candidate | lottery_slot | champion_trial"
1545
+ ).requiredOption("--chosen <action>", "the action you chose instead").requiredOption("--reason <text>", "why you overrode the recommendation").action(
1546
+ async (slug, opts) => {
1547
+ try {
1548
+ print(
1549
+ await new ErdoClient().recordAllocationDecision(slug, {
1550
+ variant_key: opts.variant,
1551
+ recommended_action: opts.recommended,
1552
+ chosen_action: opts.chosen,
1553
+ reason: opts.reason
1554
+ })
1555
+ );
1556
+ } catch (e) {
1557
+ fail(e);
1558
+ }
1559
+ }
1560
+ );
1561
+ expCmd.command("tournament <slug>").description(
1562
+ "Coalesce an experiment's variants: cluster near-dups, judge survivors pairwise, fit a Bradley\u2013Terry ranking; comparisons persist to the ledger"
1563
+ ).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(
1564
+ async (slug, opts) => {
1565
+ try {
1566
+ process.stderr.write("judging pairs \u2014 this can take a few minutes\u2026\n");
1567
+ print(
1568
+ await new ErdoClient().runPairwiseTournament(slug, {
1569
+ criteria: opts.criteria,
1570
+ kind: opts.kind,
1571
+ variant_keys: opts.variant.length ? opts.variant : void 0,
1572
+ max_comparisons: opts.maxComparisons
1573
+ })
1574
+ );
1575
+ } catch (e) {
1576
+ fail(e);
1577
+ }
1578
+ }
1579
+ );
1580
+ var judgeCmd = program.command("judge").description("Judges \u2014 back-tests and calibration");
1581
+ judgeCmd.command("backtest <judgeSlug>").description(
1582
+ "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)"
1583
+ ).action(async (judgeSlug) => {
1584
+ try {
1585
+ process.stderr.write("back-testing \u2014 this can take a few minutes\u2026\n");
1586
+ print(await new ErdoClient().backtestJudge(judgeSlug));
1587
+ } catch (e) {
1588
+ fail(e);
1589
+ }
1590
+ });
1591
+ var personaCmd = program.command("persona").description("Ground personas in measured traffic");
1592
+ personaCmd.command("ground").description(
1593
+ "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)"
1594
+ ).option("--window-days <n>", "look-back window in days (default 28)", (v) => parseInt(v, 10)).action(async (opts) => {
1595
+ try {
1596
+ print(await new ErdoClient().groundPersonas({ window_days: opts.windowDays }));
1597
+ } catch (e) {
1598
+ fail(e);
1599
+ }
1600
+ });
690
1601
  function parseRubric(json) {
691
1602
  try {
692
1603
  return JSON.parse(json);
@@ -694,10 +1605,6 @@ function parseRubric(json) {
694
1605
  throw new Error(`--rubric must be valid JSON, e.g. '[{"criterion":"brand","weight":1}]'`);
695
1606
  }
696
1607
  }
697
- var collect = (v, acc) => {
698
- acc.push(v);
699
- return acc;
700
- };
701
1608
  function caseScoringPayload(opts) {
702
1609
  const out = {};
703
1610
  if (opts.rubric) out.rubric = parseRubric(opts.rubric);
@@ -765,7 +1672,7 @@ evalCmd.command("run <slug>").description("Run a suite; --watch polls until it c
765
1672
  console.log(`run_id: ${run_id}`);
766
1673
  if (!opts.watch) return;
767
1674
  for (; ; ) {
768
- await sleep(5e3);
1675
+ await sleep2(5e3);
769
1676
  const { run, results, cases } = await api.getEvalRun(run_id);
770
1677
  if (run.status === "running") {
771
1678
  console.log(` ...running (${results.length}/${run.total_cases} cases scored)`);
@@ -861,33 +1768,32 @@ caseCmd.command("rm <slug> <caseName>").description("Archive a case from a suite
861
1768
  });
862
1769
  var agentCmd = program.command("agent").description("Run agents");
863
1770
  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)"
1771
+ "--sync",
1772
+ "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
1773
  ).action(async (question, opts) => {
867
1774
  try {
868
1775
  const client = new ErdoClient();
869
1776
  const res = await client.ask({
870
1777
  question,
871
1778
  dataset_slugs: opts.datasets ? opts.datasets.split(",").map((s) => s.trim()) : void 0,
872
- async: opts.async
1779
+ async: !opts.sync
873
1780
  });
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})`);
1781
+ if (opts.sync) {
1782
+ console.log(res.answer || `(status: ${res.status})`);
886
1783
  console.log(`
887
1784
  thread: ${res.thread_id}`);
888
1785
  return;
889
1786
  }
890
- console.log(res.answer || `(status: ${res.status})`);
1787
+ process.stderr.write(`thread: ${res.thread_id}
1788
+ running\u2026`);
1789
+ const run = await client.waitForThreadRun(res.thread_id, {
1790
+ onTick: () => process.stderr.write(".")
1791
+ });
1792
+ process.stderr.write("\n");
1793
+ if (!run) throw new Error(timedOutMessage(res.thread_id));
1794
+ if (run.status === "awaiting_approval") throw new Error(awaitingApprovalMessage(res.thread_id));
1795
+ if (run.error) throw new Error(run.error);
1796
+ console.log(run.output || `(status: ${run.status})`);
891
1797
  console.log(`
892
1798
  thread: ${res.thread_id}`);
893
1799
  } catch (e) {
@@ -895,33 +1801,32 @@ thread: ${res.thread_id}`);
895
1801
  }
896
1802
  });
897
1803
  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)"
1804
+ "--sync",
1805
+ "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
1806
  ).action(async (threadId, message, opts) => {
901
1807
  try {
902
1808
  const client = new ErdoClient();
903
- const baselineRunID = opts.async ? await client.latestRunID(threadId) : void 0;
1809
+ const baselineRunID = opts.sync ? void 0 : await client.latestRunID(threadId);
904
1810
  const res = await client.sendMessage(threadId, {
905
1811
  message,
906
1812
  agent_key: opts.agent,
907
- async: opts.async
1813
+ async: !opts.sync
908
1814
  });
909
1815
  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})`);
1816
+ if (opts.sync) {
1817
+ console.log(res.answer || `(status: ${res.status})`);
922
1818
  return;
923
1819
  }
924
- console.log(res.answer || `(status: ${res.status})`);
1820
+ process.stderr.write("running\u2026");
1821
+ const run = await client.waitForThreadRun(threadId, {
1822
+ ignoreRunID: baselineRunID,
1823
+ onTick: () => process.stderr.write(".")
1824
+ });
1825
+ process.stderr.write("\n");
1826
+ if (!run) throw new Error(timedOutMessage(threadId));
1827
+ if (run.status === "awaiting_approval") throw new Error(awaitingApprovalMessage(threadId));
1828
+ if (run.error) throw new Error(run.error);
1829
+ console.log(run.output || `(status: ${run.status})`);
925
1830
  } catch (e) {
926
1831
  fail(e);
927
1832
  }
@@ -945,6 +1850,24 @@ agentCmd.command("threads").description("List your threads").action(async () =>
945
1850
  fail(e);
946
1851
  }
947
1852
  });
1853
+ agentCmd.command("messages <threadId>").description("Show a thread's messages (role + text)").action(async (threadId) => {
1854
+ try {
1855
+ const { messages } = await new ErdoClient().getThreadMessages(threadId);
1856
+ for (const m of messages) {
1857
+ const role = m.message.author_entity_type === "app" ? "agent" : m.message.author_entity_type;
1858
+ console.log(`\u2014 ${role} \xB7 ${m.message.created_at} \u2014`);
1859
+ for (const c of m.contents) {
1860
+ if (c.content_type === "text") {
1861
+ console.log(c.content);
1862
+ } else {
1863
+ console.log(`\x1B[2m[${c.content_type}]\x1B[0m`);
1864
+ }
1865
+ }
1866
+ }
1867
+ } catch (e) {
1868
+ fail(e);
1869
+ }
1870
+ });
948
1871
  var runsCmd = program.command("runs").description("Inspect agent runs");
949
1872
  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
1873
  try {
@@ -961,6 +1884,88 @@ runsCmd.command("get <id>").description("Show an agent run (status, output, trac
961
1884
  fail(e);
962
1885
  }
963
1886
  });
1887
+ var approvalsCmd = program.command("approvals").description("List and decide approval requests (actions agents paused on)");
1888
+ 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) => {
1889
+ try {
1890
+ const res = await new ErdoClient().listApprovals(opts);
1891
+ if (opts.json) {
1892
+ print(res);
1893
+ return;
1894
+ }
1895
+ for (const r of res.requests) {
1896
+ console.log(
1897
+ `${r.id} ${r.status} ${r.action_key} ${r.action_display} ${r.created_at}`
1898
+ );
1899
+ }
1900
+ } catch (e) {
1901
+ fail(e);
1902
+ }
1903
+ });
1904
+ approvalsCmd.command("decide <id>").description("Approve or reject a pending approval request").option("--approve", "approve the request").option("--reject", "reject the request").option(
1905
+ "--scope <scope>",
1906
+ "once (default) | always_this_job | always_org | always_user",
1907
+ "once"
1908
+ ).action(async (id, opts) => {
1909
+ try {
1910
+ if (opts.approve === opts.reject) {
1911
+ throw new Error("specify exactly one of --approve or --reject");
1912
+ }
1913
+ const res = await new ErdoClient().decideApproval(id, {
1914
+ decision: opts.approve ? "approved" : "rejected",
1915
+ scope: opts.scope
1916
+ });
1917
+ console.log(res.message);
1918
+ } catch (e) {
1919
+ fail(e);
1920
+ }
1921
+ });
1922
+ var attnCmd = program.command("attention").description("The attention feed \u2014 digests, choices, escalations awaiting a human");
1923
+ 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(
1924
+ async (opts) => {
1925
+ try {
1926
+ const statuses = opts.status?.length ? opts.status : opts.open ? ["open"] : void 0;
1927
+ print(
1928
+ await new ErdoClient().listAttentionItems({
1929
+ statuses,
1930
+ engine_actions_only: opts.engineActions,
1931
+ limit: opts.limit,
1932
+ offset: opts.offset
1933
+ })
1934
+ );
1935
+ } catch (e) {
1936
+ fail(e);
1937
+ }
1938
+ }
1939
+ );
1940
+ attnCmd.command("respond <id>").description(
1941
+ `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.`
1942
+ ).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) => {
1943
+ try {
1944
+ const given = [opts.answer !== void 0, !!opts.ack, !!opts.dismiss].filter(Boolean).length;
1945
+ if (given !== 1) {
1946
+ fail(new Error("provide exactly one of --answer, --ack, or --dismiss"));
1947
+ }
1948
+ let input;
1949
+ if (opts.answer !== void 0) {
1950
+ let parsed;
1951
+ try {
1952
+ parsed = JSON.parse(opts.answer);
1953
+ } catch {
1954
+ throw new Error(
1955
+ `--answer must be valid JSON, e.g. '{"<question_id>": ["<option>", ...]}'`
1956
+ );
1957
+ }
1958
+ input = { action: "answer", answer: parsed };
1959
+ } else if (opts.ack) {
1960
+ input = { action: "acknowledge" };
1961
+ } else {
1962
+ input = { action: "dismiss" };
1963
+ }
1964
+ print(await new ErdoClient().respondAttentionItem(id, input));
1965
+ } catch (e) {
1966
+ fail(e);
1967
+ }
1968
+ });
964
1969
  var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
965
1970
  function readMaybeFile(v) {
966
1971
  if (!v) return void 0;
@@ -1066,6 +2071,172 @@ datasetsCmd.command("query <slug> <question>").description("Ask a natural-langua
1066
2071
  fail(e);
1067
2072
  }
1068
2073
  });
2074
+ var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
2075
+ 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) => {
2076
+ try {
2077
+ const content = readFileSync2(file);
2078
+ if (content.byteLength > MAX_UPLOAD_BYTES) {
2079
+ fail(
2080
+ new Error(
2081
+ `${file} is ${Math.round(content.byteLength / (1024 * 1024))} MB \u2014 the upload limit is 20 MB; upload larger files through the Erdo web app`
2082
+ )
2083
+ );
2084
+ }
2085
+ const filename = basename(file);
2086
+ const res = await new ErdoClient().uploadDatasetFile({
2087
+ filename,
2088
+ content_base64: content.toString("base64"),
2089
+ name: opts.name,
2090
+ description: opts.description
2091
+ });
2092
+ console.log(`Created dataset ${res.slug} (${res.dataset_id})`);
2093
+ if (res.ready === false) {
2094
+ console.log(`Warning: ${res.warning ?? "the file's schema could not be extracted; it is not yet queryable."}`);
2095
+ } else {
2096
+ console.log(`Query it with: erdo datasets query ${res.slug} "..."`);
2097
+ }
2098
+ } catch (e) {
2099
+ fail(e);
2100
+ }
2101
+ });
2102
+ 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) => {
2103
+ try {
2104
+ const res = await new ErdoClient().createIntegrationDataset({
2105
+ integration: app,
2106
+ name: opts.name,
2107
+ description: opts.description,
2108
+ schemas: opts.schemas ? opts.schemas.split(",").map((s) => s.trim()) : void 0
2109
+ });
2110
+ console.log(`Created dataset ${res.slug} (${res.dataset_id}), status: ${res.status}`);
2111
+ console.log(`Table discovery runs in the background \u2014 query shortly with: erdo datasets query ${res.slug} "..."`);
2112
+ } catch (e) {
2113
+ fail(e);
2114
+ }
2115
+ });
2116
+ var OPERATORS = ["equals", "not_equals", "greater_than", "less_than", "contains", "between"];
2117
+ function parseCondition(s) {
2118
+ const m = s.trim().match(/^(\S+)\s+(\S+)\s+([\s\S]+)$/);
2119
+ if (!m) {
2120
+ fail(new Error(`invalid --where ${JSON.stringify(s)}; expected '<column> <operator> <value>', e.g. 'email contains @example.com'`));
2121
+ }
2122
+ const [, column, operator, value] = m;
2123
+ if (!OPERATORS.includes(operator)) {
2124
+ fail(new Error(`unsupported operator ${JSON.stringify(operator)}; allowed: ${OPERATORS.join(", ")}`));
2125
+ }
2126
+ return { column, operator, value };
2127
+ }
2128
+ var filterCmd = datasetsCmd.command("filter").description("Manage a dataset's default filters");
2129
+ filterCmd.command("list <slug>").description("List the default filters on a dataset").action(async (slug) => {
2130
+ try {
2131
+ print(await new ErdoClient().listDatasetFilters(slug));
2132
+ } catch (e) {
2133
+ fail(e);
2134
+ }
2135
+ });
2136
+ var integrationsCmd = program.command("integrations").description("Connect and inspect integrations");
2137
+ integrationsCmd.command("list").description("List connected integrations").action(async () => {
2138
+ try {
2139
+ const { integrations } = await new ErdoClient().listIntegrations();
2140
+ for (const i of integrations) console.log(`${i.app} ${i.status} ${i.auth_type} ${i.name}`);
2141
+ } catch (e) {
2142
+ fail(e);
2143
+ }
2144
+ });
2145
+ integrationsCmd.command("apps [query]").description("Search connectable apps (native integrations + SaaS apps)").action(async (query) => {
2146
+ try {
2147
+ const { apps } = await new ErdoClient().searchIntegrationApps(query);
2148
+ for (const a of apps) console.log(`${a.app} ${a.source} ${a.auth_types.join(",")} ${a.name}`);
2149
+ } catch (e) {
2150
+ fail(e);
2151
+ }
2152
+ });
2153
+ 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) => {
2154
+ try {
2155
+ let credentials;
2156
+ if (opts.credential.length) {
2157
+ credentials = {};
2158
+ for (const c of opts.credential) {
2159
+ const eq = c.indexOf("=");
2160
+ if (eq < 1) fail(new Error(`invalid --credential ${JSON.stringify(c)}; expected key=value`));
2161
+ credentials[c.slice(0, eq)] = c.slice(eq + 1);
2162
+ }
2163
+ }
2164
+ const res = await new ErdoClient().connectIntegration({ app, name: opts.name, credentials });
2165
+ print(res);
2166
+ if (res.connect_url) {
2167
+ console.error(`
2168
+ Open this URL in a browser to authorize, then run: erdo integrations status ${app}`);
2169
+ }
2170
+ } catch (e) {
2171
+ fail(e);
2172
+ }
2173
+ });
2174
+ integrationsCmd.command("status <app>").description("Check whether an app is connected (completes browser-authorized connections)").action(async (app) => {
2175
+ try {
2176
+ print(await new ErdoClient().checkIntegrationConnection(app));
2177
+ } catch (e) {
2178
+ fail(e);
2179
+ }
2180
+ });
2181
+ integrationsCmd.command("tables <app> [schema]").description("List a connected database integration's schemas, or a schema's tables with columns").action(async (app, schema) => {
2182
+ try {
2183
+ const res = await new ErdoClient().discoverIntegrationTables(app, schema);
2184
+ if (!schema) {
2185
+ for (const s of res.schemas) console.log(`${s.name} ${s.type}`);
2186
+ if (res.schemas.length) console.log(`
2187
+ List tables with: erdo integrations tables ${app} <schema>`);
2188
+ return;
2189
+ }
2190
+ for (const t of res.tables ?? []) {
2191
+ const rows = t.estimated_row_count != null ? ` ~${t.estimated_row_count} rows` : "";
2192
+ console.log(`${t.schema_name}.${t.table_name} ${t.columns.length} columns${rows}`);
2193
+ }
2194
+ } catch (e) {
2195
+ fail(e);
2196
+ }
2197
+ });
2198
+ var pipelinesCmd = program.command("event-pipelines").description("Inspect event pipelines (lead-capture / webhook flows)");
2199
+ pipelinesCmd.command("list").description("List event pipelines in the active org").option("-n, --limit <n>", "max rows", (v) => parseInt(v, 10)).action(async (opts) => {
2200
+ try {
2201
+ print(await new ErdoClient().listEventPipelines(opts.limit));
2202
+ } catch (e) {
2203
+ fail(e);
2204
+ }
2205
+ });
2206
+ pipelinesCmd.command("get <slug>").description("Show one event pipeline \u2014 state, source, endpoint URL, actions, write targets").action(async (slug) => {
2207
+ try {
2208
+ print(await new ErdoClient().getEventPipeline(slug));
2209
+ } catch (e) {
2210
+ fail(e);
2211
+ }
2212
+ });
2213
+ filterCmd.command("add <slug>").description("Add a default filter to a dataset").requiredOption(
2214
+ "--where <condition>",
2215
+ "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')",
2216
+ collect,
2217
+ []
2218
+ ).option("--name <name>", "optional human-readable label, e.g. 'Exclude test submissions'").action(async (slug, opts) => {
2219
+ try {
2220
+ const conditions = opts.where.map(parseCondition);
2221
+ print(await new ErdoClient().createDatasetFilter(slug, { name: opts.name, conditions }));
2222
+ } catch (e) {
2223
+ fail(e);
2224
+ }
2225
+ });
2226
+ 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) => {
2227
+ try {
2228
+ print(await new ErdoClient().deleteDatasetFilter(slug, filterId));
2229
+ } catch (e) {
2230
+ fail(e);
2231
+ }
2232
+ });
2233
+ 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) => {
2234
+ try {
2235
+ print(await new ErdoClient().listEventPipelineExecutions(slug, opts.limit));
2236
+ } catch (e) {
2237
+ fail(e);
2238
+ }
2239
+ });
1069
2240
  var knowledgeCmd = program.command("knowledge").description("Knowledge objects");
1070
2241
  knowledgeCmd.command("list").description("List knowledge objects").action(async () => {
1071
2242
  try {
@@ -1081,6 +2252,15 @@ knowledgeCmd.command("search <query>").description("Semantic search over knowled
1081
2252
  fail(e);
1082
2253
  }
1083
2254
  });
2255
+ knowledgeCmd.command("visibility <id> <visibility>").description(
2256
+ "Set a knowledge object's visibility: 'workspace' (organization-internal) or 'public' (answerable by external surfaces like the website voice widget once approved)"
2257
+ ).action(async (id, visibility) => {
2258
+ try {
2259
+ print(await new ErdoClient().setKnowledgeVisibility(id, visibility));
2260
+ } catch (e) {
2261
+ fail(e);
2262
+ }
2263
+ });
1084
2264
  var autoCmd = program.command("automations").description("Scheduled automations (heartbeats)");
1085
2265
  autoCmd.command("list").description("List automations").action(async () => {
1086
2266
  try {