@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.
- package/dist/index.js +1241 -63
- 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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
if (
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
"--
|
|
865
|
-
"
|
|
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.
|
|
1777
|
+
async: !opts.sync
|
|
873
1778
|
});
|
|
874
|
-
if (opts.
|
|
875
|
-
|
|
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
|
-
|
|
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
|
-
"--
|
|
899
|
-
"
|
|
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.
|
|
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.
|
|
1811
|
+
async: !opts.sync
|
|
908
1812
|
});
|
|
909
1813
|
if (res.error) throw new Error(res.error);
|
|
910
|
-
if (opts.
|
|
911
|
-
|
|
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
|
-
|
|
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 {
|