@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.
- package/dist/index.js +1245 -65
- 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
|
|
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(
|
|
@@ -268,12 +490,83 @@ var ErdoClient = class {
|
|
|
268
490
|
"/v1/datasets"
|
|
269
491
|
);
|
|
270
492
|
}
|
|
271
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
if (
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
"--
|
|
865
|
-
"
|
|
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.
|
|
1779
|
+
async: !opts.sync
|
|
873
1780
|
});
|
|
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})`);
|
|
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
|
-
|
|
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
|
-
"--
|
|
899
|
-
"
|
|
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.
|
|
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.
|
|
1813
|
+
async: !opts.sync
|
|
908
1814
|
});
|
|
909
1815
|
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})`);
|
|
1816
|
+
if (opts.sync) {
|
|
1817
|
+
console.log(res.answer || `(status: ${res.status})`);
|
|
922
1818
|
return;
|
|
923
1819
|
}
|
|
924
|
-
|
|
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 {
|