@erdoai/cli 0.44.0 → 0.46.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/README.md +58 -11
- package/dist/index.js +191 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,14 @@ erdo update # self-update to the latest published version
|
|
|
13
13
|
|
|
14
14
|
Requires Node.js 18+. User-facing install docs: https://docs.erdo.ai/cli
|
|
15
15
|
|
|
16
|
+
When a newer version is published, every command prints one line on **stderr**
|
|
17
|
+
naming the installed version and `erdo update`. It is on stderr so stdout stays
|
|
18
|
+
clean JSON for scripts and agents, the published version is remembered on disk
|
|
19
|
+
and re-checked about once a day, and an unreachable registry says nothing at
|
|
20
|
+
all. Set `ERDO_NO_UPDATE_NOTIFIER=1` to silence it. An old build's `--help` is
|
|
21
|
+
correct about itself and wrong about the product — that is the failure this
|
|
22
|
+
notice exists to stop.
|
|
23
|
+
|
|
16
24
|
## Auth
|
|
17
25
|
|
|
18
26
|
Multi-account, like `gh`. Log in with an API key (created in Erdo); it's stored
|
|
@@ -34,6 +42,47 @@ erdo --org acme eval suites # one-off override for a single command
|
|
|
34
42
|
Env overrides for CI/scripting: `ERDO_API_KEY`, `ERDO_ORG`, `ERDO_API_URL`,
|
|
35
43
|
`ERDO_ACCOUNT`.
|
|
36
44
|
|
|
45
|
+
## Datasets
|
|
46
|
+
|
|
47
|
+
Two ways to read a dataset, and the difference matters.
|
|
48
|
+
|
|
49
|
+
**`datasets fetch` is the read.** You write the SQL, so the same command gives
|
|
50
|
+
the same rows every time — use it for anything mechanical, scripted, or run by
|
|
51
|
+
an agent. It answers with `{columns, rows, row_count}`.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
erdo --org 2200-brickell datasets fetch 2200-brickell.page-events \
|
|
55
|
+
--sql "SELECT event, count(*) FROM data GROUP BY 1 ORDER BY 2 DESC" --limit 20
|
|
56
|
+
|
|
57
|
+
erdo datasets fetch acme.leads --limit 50 # no SQL: just the rows
|
|
58
|
+
erdo datasets fetch acme.leads --filter <name> # opt into a saved filter
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The table is named **`data`** — for file datasets (CSV, Excel, and anything
|
|
62
|
+
written by an event pipeline) that is the name regardless of the dataset's slug
|
|
63
|
+
or resource key, and it is not guessable, so it is the first thing to get right.
|
|
64
|
+
Database and warehouse datasets are queried through their real table names, which
|
|
65
|
+
come from the dataset's schema. The SQL dialect is DuckDB. A dataset's default
|
|
66
|
+
filters apply on every read; `--filter <name>` adds a saved filter on top, and
|
|
67
|
+
narrows further — a named filter never bypasses a default. See
|
|
68
|
+
`erdo datasets filter list <slug>` for the names a dataset offers.
|
|
69
|
+
|
|
70
|
+
**`datasets query` is the natural-language wrapper.** Erdo writes and runs the
|
|
71
|
+
SQL for you, and answers with that SQL alongside the values, so it is the one to
|
|
72
|
+
reach for when you don't yet know the shape of the data. It runs an agent, so it
|
|
73
|
+
is slower and it is not deterministic — two identical questions can produce two
|
|
74
|
+
different queries.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
erdo --org 2200-brickell datasets query 2200-brickell.page-events \
|
|
78
|
+
"which pages get the most views, and how many convert?"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
erdo datasets list # slug, type, status, class, name
|
|
83
|
+
erdo datasets upload ./leads.csv # create a dataset from a file
|
|
84
|
+
```
|
|
85
|
+
|
|
37
86
|
## Agents & pages
|
|
38
87
|
|
|
39
88
|
```bash
|
|
@@ -86,15 +135,13 @@ npm run build # bundle to dist/ (bin: erdo)
|
|
|
86
135
|
|
|
87
136
|
## Release
|
|
88
137
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
git tag cli-v0.4.1
|
|
96
|
-
git push origin cli-v0.4.1
|
|
97
|
-
```
|
|
138
|
+
**The version in `cli/package.json` is the release.** Bump it in the same PR as
|
|
139
|
+
the change (semver: `feat` → minor, `fix` → patch) and merge;
|
|
140
|
+
`.github/workflows/publish-cli.yml` publishes whatever `main` says and tags the
|
|
141
|
+
result. There is no tag to remember — forgetting one is how the CLI sat at npm
|
|
142
|
+
0.5.1 while `package.json` read 0.23.0 and every work-engine command was
|
|
143
|
+
unreachable for anyone who installed it.
|
|
98
144
|
|
|
99
|
-
|
|
100
|
-
`cli
|
|
145
|
+
npm itself is the guard: an already-published version stops the job before it
|
|
146
|
+
builds, so a `cli/` change with no bump, a re-run, and a revert all no-op. A
|
|
147
|
+
change under `cli/` that ships no bump ships to nobody.
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5
5
|
import { basename } from "path";
|
|
6
6
|
import { select } from "@inquirer/prompts";
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -14,6 +14,9 @@ var DEFAULT_API_URL = "https://api.erdo.ai";
|
|
|
14
14
|
var CONFIG_DIR = join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "erdo");
|
|
15
15
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
16
16
|
var LEGACY_KEY = "default";
|
|
17
|
+
function configPath(name) {
|
|
18
|
+
return join(CONFIG_DIR, name);
|
|
19
|
+
}
|
|
17
20
|
function loadConfigFile() {
|
|
18
21
|
if (!existsSync(CONFIG_PATH)) return null;
|
|
19
22
|
try {
|
|
@@ -284,6 +287,13 @@ var ErdoClient = class {
|
|
|
284
287
|
queryPageAnalytics(query) {
|
|
285
288
|
return this.request("POST", "/v1/page-analytics/query", { query });
|
|
286
289
|
}
|
|
290
|
+
// Read which analytics destinations the org's published pages send visitor data
|
|
291
|
+
// to — the check that stops "this audience is big enough to retarget" being
|
|
292
|
+
// concluded from traffic no pixel was ever tagging. Read-only: destinations are
|
|
293
|
+
// Erdo-provisioned, never caller-set.
|
|
294
|
+
getPageTracking() {
|
|
295
|
+
return this.request("GET", "/v1/page-tracking");
|
|
296
|
+
}
|
|
287
297
|
listEvalSuites() {
|
|
288
298
|
return this.request("GET", "/v1/evals/suites");
|
|
289
299
|
}
|
|
@@ -535,6 +545,7 @@ var ErdoClient = class {
|
|
|
535
545
|
if (params?.offset) q.set("offset", String(params.offset));
|
|
536
546
|
if (params?.categories) q.set("categories", params.categories);
|
|
537
547
|
if (params?.scope) q.set("scope", params.scope);
|
|
548
|
+
if (params?.workstream_slug) q.set("workstream_slug", params.workstream_slug);
|
|
538
549
|
const qs = q.toString();
|
|
539
550
|
return this.request(
|
|
540
551
|
"GET",
|
|
@@ -656,6 +667,10 @@ var ErdoClient = class {
|
|
|
656
667
|
}
|
|
657
668
|
// The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
|
|
658
669
|
// `query` made every `erdo datasets query` fail with "question is required".
|
|
670
|
+
//
|
|
671
|
+
// `rows` carries the values in `columns` order — the answer to the question —
|
|
672
|
+
// and `output` the same result rendered to read. `row_count` is how many rows
|
|
673
|
+
// the generated SQL matched and can exceed `rows.length`, which is capped.
|
|
659
674
|
queryDataset(slug, question, timezone) {
|
|
660
675
|
return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/query-nl`, {
|
|
661
676
|
question,
|
|
@@ -850,6 +865,7 @@ var ErdoClient = class {
|
|
|
850
865
|
const params = new URLSearchParams();
|
|
851
866
|
if (opts.status) params.set("status", opts.status);
|
|
852
867
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
868
|
+
if (opts.workstream_slug) params.set("workstream_slug", opts.workstream_slug);
|
|
853
869
|
const qs = params.toString();
|
|
854
870
|
return this.request(
|
|
855
871
|
"GET",
|
|
@@ -1090,9 +1106,14 @@ async function browserLogin() {
|
|
|
1090
1106
|
}
|
|
1091
1107
|
|
|
1092
1108
|
// src/update.ts
|
|
1093
|
-
import { spawnSync } from "child_process";
|
|
1109
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
1110
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1111
|
+
import { dirname as dirname2 } from "path";
|
|
1094
1112
|
var PKG = "@erdoai/cli";
|
|
1095
1113
|
var MANUAL_HINT = `npm install -g ${PKG}@latest`;
|
|
1114
|
+
var CACHE_PATH = configPath("update-check.json");
|
|
1115
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1116
|
+
var REFRESH_COMMAND = "__update-check";
|
|
1096
1117
|
function parseVersion(v) {
|
|
1097
1118
|
const [core, pre = ""] = v.split("-", 2);
|
|
1098
1119
|
const nums = core.split(".").map((s) => Number.parseInt(s, 10) || 0);
|
|
@@ -1111,19 +1132,65 @@ function isNewer(latest, current) {
|
|
|
1111
1132
|
if (a.pre && !b.pre) return false;
|
|
1112
1133
|
return a.pre > b.pre;
|
|
1113
1134
|
}
|
|
1114
|
-
async function
|
|
1115
|
-
let latest;
|
|
1135
|
+
async function fetchLatestVersion(timeoutMs) {
|
|
1116
1136
|
try {
|
|
1117
1137
|
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
|
|
1118
|
-
signal: AbortSignal.timeout(
|
|
1138
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1119
1139
|
});
|
|
1120
|
-
if (res.ok)
|
|
1140
|
+
if (res.ok) return (await res.json()).version;
|
|
1141
|
+
} catch {
|
|
1142
|
+
}
|
|
1143
|
+
return void 0;
|
|
1144
|
+
}
|
|
1145
|
+
function readVersionCache() {
|
|
1146
|
+
try {
|
|
1147
|
+
if (!existsSync2(CACHE_PATH)) return {};
|
|
1148
|
+
const raw = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
|
|
1149
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
1121
1150
|
} catch {
|
|
1151
|
+
return {};
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function writeVersionCache(cache) {
|
|
1155
|
+
try {
|
|
1156
|
+
mkdirSync2(dirname2(CACHE_PATH), { recursive: true });
|
|
1157
|
+
writeFileSync2(CACHE_PATH, `${JSON.stringify(cache)}
|
|
1158
|
+
`);
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function refreshVersionCache() {
|
|
1163
|
+
const latest = await fetchLatestVersion(1e4);
|
|
1164
|
+
if (latest) writeVersionCache({ latest, checkedAt: Date.now() });
|
|
1165
|
+
}
|
|
1166
|
+
function notifyIfOutdated(current) {
|
|
1167
|
+
if (process.env.ERDO_NO_UPDATE_NOTIFIER) return;
|
|
1168
|
+
const cache = readVersionCache();
|
|
1169
|
+
if (cache.latest && isNewer(cache.latest, current)) {
|
|
1170
|
+
console.error(
|
|
1171
|
+
`erdo v${current} is out of date (v${cache.latest} is published) \u2014 run \`erdo update\`.`
|
|
1172
|
+
);
|
|
1122
1173
|
}
|
|
1174
|
+
const age = Date.now() - (cache.checkedAt ?? 0);
|
|
1175
|
+
if (age < CHECK_INTERVAL_MS) return;
|
|
1176
|
+
writeVersionCache({ ...cache, checkedAt: Date.now() });
|
|
1177
|
+
try {
|
|
1178
|
+
const entry = process.argv[1];
|
|
1179
|
+
if (!entry) return;
|
|
1180
|
+
spawn2(process.execPath, [entry, REFRESH_COMMAND], {
|
|
1181
|
+
detached: true,
|
|
1182
|
+
stdio: "ignore"
|
|
1183
|
+
}).unref();
|
|
1184
|
+
} catch {
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
async function update(current) {
|
|
1188
|
+
const latest = await fetchLatestVersion(1e4);
|
|
1123
1189
|
if (!latest) {
|
|
1124
1190
|
console.error(`Could not reach the npm registry. Try manually: ${MANUAL_HINT}`);
|
|
1125
1191
|
process.exit(1);
|
|
1126
1192
|
}
|
|
1193
|
+
writeVersionCache({ latest, checkedAt: Date.now() });
|
|
1127
1194
|
if (!isNewer(latest, current)) {
|
|
1128
1195
|
console.log(`Already up to date (v${current}).`);
|
|
1129
1196
|
return;
|
|
@@ -1146,7 +1213,7 @@ async function update(current) {
|
|
|
1146
1213
|
}
|
|
1147
1214
|
|
|
1148
1215
|
// src/index.ts
|
|
1149
|
-
var pkg = JSON.parse(
|
|
1216
|
+
var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
|
|
1150
1217
|
function fail(err) {
|
|
1151
1218
|
if (err instanceof ErdoApiError) {
|
|
1152
1219
|
console.error(`Error ${err.status}: ${err.message}`);
|
|
@@ -1307,6 +1374,9 @@ program.command("update").description("Update the CLI to the latest published ve
|
|
|
1307
1374
|
fail(e);
|
|
1308
1375
|
}
|
|
1309
1376
|
});
|
|
1377
|
+
program.command(REFRESH_COMMAND, { hidden: true }).description("Internal: refresh the remembered latest published version").action(async () => {
|
|
1378
|
+
await refreshVersionCache();
|
|
1379
|
+
});
|
|
1310
1380
|
var org = program.command("org").description("Manage the active organization");
|
|
1311
1381
|
org.command("list").description("List your organizations (* = active)").action(async () => {
|
|
1312
1382
|
try {
|
|
@@ -2309,9 +2379,13 @@ runsCmd.command("get <id>").description("Show an agent run (status, output, trac
|
|
|
2309
2379
|
}
|
|
2310
2380
|
});
|
|
2311
2381
|
var approvalsCmd = program.command("approvals").description("List and decide approval requests (actions agents paused on)");
|
|
2312
|
-
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) => {
|
|
2382
|
+
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("--workstream <slug>", "only approvals for this workstream").option("--json", "output raw JSON").action(async (opts) => {
|
|
2313
2383
|
try {
|
|
2314
|
-
const res = await new ErdoClient().listApprovals(
|
|
2384
|
+
const res = await new ErdoClient().listApprovals({
|
|
2385
|
+
status: opts.status,
|
|
2386
|
+
limit: opts.limit,
|
|
2387
|
+
workstream_slug: opts.workstream
|
|
2388
|
+
});
|
|
2315
2389
|
if (opts.json) {
|
|
2316
2390
|
print(res);
|
|
2317
2391
|
return;
|
|
@@ -2328,22 +2402,59 @@ approvalsCmd.command("list").description("List approval requests, optionally fil
|
|
|
2328
2402
|
});
|
|
2329
2403
|
approvalsCmd.command("decide <id>").description("Approve or reject a pending approval request").option("--approve", "approve the request").option("--reject", "reject the request").option(
|
|
2330
2404
|
"--scope <scope>",
|
|
2331
|
-
"once (default) | always_this_job | always_org | always_user",
|
|
2405
|
+
"once (default) | always_this_job | always_this_workstream | always_org | always_user",
|
|
2332
2406
|
"once"
|
|
2333
|
-
).
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2407
|
+
).option(
|
|
2408
|
+
"--option <n>",
|
|
2409
|
+
"for standing (non-once) approvals: use the request's Nth scope option (1-based) as the policy's parameter constraints \u2014 list them with `erdo approvals list --json`",
|
|
2410
|
+
(v) => parseInt(v, 10)
|
|
2411
|
+
).option(
|
|
2412
|
+
"--constraints <json>",
|
|
2413
|
+
`explicit parameter constraints for the standing policy, e.g. '{"spreadsheet_id":{"values":["..."]}}'`
|
|
2414
|
+
).action(
|
|
2415
|
+
async (id, opts) => {
|
|
2416
|
+
try {
|
|
2417
|
+
if (opts.approve === opts.reject) {
|
|
2418
|
+
throw new Error("specify exactly one of --approve or --reject");
|
|
2419
|
+
}
|
|
2420
|
+
if (opts.option !== void 0 && opts.constraints !== void 0) {
|
|
2421
|
+
throw new Error("specify at most one of --option or --constraints");
|
|
2422
|
+
}
|
|
2423
|
+
const client = new ErdoClient();
|
|
2424
|
+
let constraints;
|
|
2425
|
+
if (opts.constraints !== void 0) {
|
|
2426
|
+
try {
|
|
2427
|
+
constraints = JSON.parse(opts.constraints);
|
|
2428
|
+
} catch {
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
`--constraints must be JSON, e.g. '{"spreadsheet_id":{"values":["abc"]}}'`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
} else if (opts.option !== void 0) {
|
|
2434
|
+
const res2 = await client.listApprovals({ status: "pending" });
|
|
2435
|
+
const req = res2.requests.find((r) => r.id === id);
|
|
2436
|
+
if (!req) {
|
|
2437
|
+
throw new Error(`pending approval ${id} not found`);
|
|
2438
|
+
}
|
|
2439
|
+
const options = req.scope_options ?? [];
|
|
2440
|
+
if (opts.option < 1 || opts.option > options.length) {
|
|
2441
|
+
const listing = options.length ? options.map((o, i) => ` ${i + 1}. ${o.label}`).join("\n") : " (none \u2014 pass --constraints instead)";
|
|
2442
|
+
throw new Error(`--option must be 1..${options.length}; available options:
|
|
2443
|
+
${listing}`);
|
|
2444
|
+
}
|
|
2445
|
+
constraints = options[opts.option - 1].constraints;
|
|
2446
|
+
}
|
|
2447
|
+
const res = await client.decideApproval(id, {
|
|
2448
|
+
decision: opts.approve ? "approved" : "rejected",
|
|
2449
|
+
scope: opts.scope,
|
|
2450
|
+
parameter_constraints: constraints
|
|
2451
|
+
});
|
|
2452
|
+
console.log(res.message);
|
|
2453
|
+
} catch (e) {
|
|
2454
|
+
fail(e);
|
|
2337
2455
|
}
|
|
2338
|
-
const res = await new ErdoClient().decideApproval(id, {
|
|
2339
|
-
decision: opts.approve ? "approved" : "rejected",
|
|
2340
|
-
scope: opts.scope
|
|
2341
|
-
});
|
|
2342
|
-
console.log(res.message);
|
|
2343
|
-
} catch (e) {
|
|
2344
|
-
fail(e);
|
|
2345
2456
|
}
|
|
2346
|
-
|
|
2457
|
+
);
|
|
2347
2458
|
var attnCmd = program.command("attention").description("The attention feed \u2014 digests, choices, escalations awaiting a human");
|
|
2348
2459
|
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(
|
|
2349
2460
|
async (opts) => {
|
|
@@ -2466,14 +2577,15 @@ program.command("activity").alias("feed").description(
|
|
|
2466
2577
|
).option("-n, --limit <n>", "max items (default 20, max 100)", (v) => parseInt(v, 10)).option("--offset <n>", "pagination offset", (v) => parseInt(v, 10)).option(
|
|
2467
2578
|
"--categories <list>",
|
|
2468
2579
|
"comma-separated: attention,approval,workstream,catalog,job,heartbeat (default: attention,approval,workstream)"
|
|
2469
|
-
).option("--scope <scope>", "following | all (default all)").option("--json", "raw JSON response").action(
|
|
2580
|
+
).option("--scope <scope>", "following | all (default all)").option("--workstream <slug>", "only this workstream").option("--json", "raw JSON response").action(
|
|
2470
2581
|
async (opts) => {
|
|
2471
2582
|
try {
|
|
2472
2583
|
const feed = await new ErdoClient().listActivityFeed({
|
|
2473
2584
|
limit: opts.limit,
|
|
2474
2585
|
offset: opts.offset,
|
|
2475
2586
|
categories: opts.categories,
|
|
2476
|
-
scope: opts.scope
|
|
2587
|
+
scope: opts.scope,
|
|
2588
|
+
workstream_slug: opts.workstream
|
|
2477
2589
|
});
|
|
2478
2590
|
if (opts.json) {
|
|
2479
2591
|
print(feed);
|
|
@@ -2676,7 +2788,7 @@ reviewsCmd.command("decide <id>").description(
|
|
|
2676
2788
|
var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
|
|
2677
2789
|
function readMaybeFile(v) {
|
|
2678
2790
|
if (!v) return void 0;
|
|
2679
|
-
return v.startsWith("@") ?
|
|
2791
|
+
return v.startsWith("@") ? readFileSync3(v.slice(1), "utf8") : v;
|
|
2680
2792
|
}
|
|
2681
2793
|
function grantList(v) {
|
|
2682
2794
|
if (v === void 0) return void 0;
|
|
@@ -2855,14 +2967,21 @@ datasetsCmd.command("list").description("List datasets").option(
|
|
|
2855
2967
|
fail(e);
|
|
2856
2968
|
}
|
|
2857
2969
|
});
|
|
2858
|
-
datasetsCmd.command("query <slug> <question>").description(
|
|
2970
|
+
datasetsCmd.command("query <slug> <question>").description(
|
|
2971
|
+
"Ask a natural-language question of a dataset \u2014 Erdo writes and runs the SQL, and answers with that SQL alongside the values. It runs an agent, so it is slower and two identical questions can produce two different queries: for a deterministic or scripted read, write the SQL yourself with `datasets fetch --sql`."
|
|
2972
|
+
).action(async (slug, question) => {
|
|
2859
2973
|
try {
|
|
2860
2974
|
print(await new ErdoClient().queryDataset(slug, question));
|
|
2861
2975
|
} catch (e) {
|
|
2862
2976
|
fail(e);
|
|
2863
2977
|
}
|
|
2864
2978
|
});
|
|
2865
|
-
datasetsCmd.command("fetch <slug>").description(
|
|
2979
|
+
datasetsCmd.command("fetch <slug>").description(
|
|
2980
|
+
"Read rows from a dataset \u2014 the deterministic read, and the one to use for anything mechanical or scripted. Returns {columns, rows, row_count}."
|
|
2981
|
+
).option(
|
|
2982
|
+
"-q, --sql <query>",
|
|
2983
|
+
"DuckDB SQL shaping the rows. File datasets (including anything an event pipeline writes) are queried as a table named `data`, whatever the dataset's slug; database and warehouse datasets use their real table names from the schema."
|
|
2984
|
+
).option("-l, --limit <n>", "max rows to return", (v) => parseInt(v, 10)).option(
|
|
2866
2985
|
"-f, --filter <name>",
|
|
2867
2986
|
"opt into a saved filter by name (repeatable); additive on top of the dataset's default filters (see: erdo datasets filter list <slug>)",
|
|
2868
2987
|
(v, acc) => acc.concat(v),
|
|
@@ -2883,7 +3002,7 @@ datasetsCmd.command("fetch <slug>").description("Fetch rows from a dataset, opti
|
|
|
2883
3002
|
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
2884
3003
|
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) => {
|
|
2885
3004
|
try {
|
|
2886
|
-
const content =
|
|
3005
|
+
const content = readFileSync3(file);
|
|
2887
3006
|
if (content.byteLength > MAX_UPLOAD_BYTES) {
|
|
2888
3007
|
fail(
|
|
2889
3008
|
new Error(
|
|
@@ -2944,7 +3063,7 @@ function printAnalyticsTable(columns, rows) {
|
|
|
2944
3063
|
console.log(widths.map((w) => "-".repeat(w)).join(" "));
|
|
2945
3064
|
for (const r of rows) console.log(line(columns.map((_, i) => cell(r[i]))));
|
|
2946
3065
|
}
|
|
2947
|
-
var analytics = program.command("analytics").description("
|
|
3066
|
+
var analytics = program.command("analytics").description("Page analytics \u2014 what is tracking your published pages, and how they perform with real visitors");
|
|
2948
3067
|
analytics.command("query <hogql>").description("Run a read-only HogQL query against this org's page-analytics events").option("--json", "print the raw JSON result instead of a table").action(async (hogql, opts) => {
|
|
2949
3068
|
try {
|
|
2950
3069
|
const res = await new ErdoClient().queryPageAnalytics(hogql);
|
|
@@ -2963,6 +3082,44 @@ analytics.command("query <hogql>").description("Run a read-only HogQL query agai
|
|
|
2963
3082
|
fail(e);
|
|
2964
3083
|
}
|
|
2965
3084
|
});
|
|
3085
|
+
var TRACKING_KINDS = [
|
|
3086
|
+
{ kind: "session_analytics", what: "heatmaps, session replay, and the events behind `erdo analytics query`" },
|
|
3087
|
+
{ kind: "google_analytics", what: "GA4 measurement" },
|
|
3088
|
+
{ kind: "meta_pixel", what: "cookies visitors so Meta retargeting audiences can be built" }
|
|
3089
|
+
];
|
|
3090
|
+
analytics.command("tracking").description("Show which analytics destinations this org's published pages send visitor data to").option("--json", "print the raw JSON result instead of a table").action(async (opts) => {
|
|
3091
|
+
try {
|
|
3092
|
+
const res = await new ErdoClient().getPageTracking();
|
|
3093
|
+
if (opts.json) {
|
|
3094
|
+
print(res);
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
const destinations = res.destinations ?? [];
|
|
3098
|
+
const byKind = new Map(destinations.map((d) => [d.kind, d]));
|
|
3099
|
+
const rows = TRACKING_KINDS.map(({ kind, what }) => {
|
|
3100
|
+
const d = byKind.get(kind);
|
|
3101
|
+
const status = d === void 0 ? "not configured" : d.enabled ? "on" : "configured, off";
|
|
3102
|
+
return [kind, status, d?.public_id ?? "", d?.provider ?? "", what];
|
|
3103
|
+
});
|
|
3104
|
+
for (const d of destinations) {
|
|
3105
|
+
if (!TRACKING_KINDS.some((k) => k.kind === d.kind)) {
|
|
3106
|
+
rows.push([d.kind, d.enabled ? "on" : "configured, off", d.public_id ?? "", d.provider ?? "", ""]);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
printAnalyticsTable(["kind", "status", "public id", "provider", "what it does"], rows);
|
|
3110
|
+
console.log(`
|
|
3111
|
+
session replay input masking: ${res.mask_inputs ? "on" : "OFF \u2014 form values are recorded"}`);
|
|
3112
|
+
console.log(`consent: ${res.consent}${res.consent === "required" ? " (recording waits on a consent banner, so thin volume may be the gate, not the traffic)" : ""}`);
|
|
3113
|
+
if (!byKind.get("meta_pixel")?.enabled) {
|
|
3114
|
+
console.log(`
|
|
3115
|
+
No Meta pixel is firing on these pages, so no retargeting audience is accumulating \u2014 page traffic alone does not mean those visitors can be served ads.`);
|
|
3116
|
+
}
|
|
3117
|
+
console.log(`
|
|
3118
|
+
Erdo's own page-events beacon is provisioned per page at render time and is not listed above \u2014 it records independently of every vendor here.`);
|
|
3119
|
+
} catch (e) {
|
|
3120
|
+
fail(e);
|
|
3121
|
+
}
|
|
3122
|
+
});
|
|
2966
3123
|
var OPERATORS = ["equals", "not_equals", "greater_than", "less_than", "contains", "not_contains", "between"];
|
|
2967
3124
|
function parseCondition(s) {
|
|
2968
3125
|
const m = s.trim().match(/^(\S+)\s+(\S+)\s+([\s\S]+)$/);
|
|
@@ -3191,7 +3348,7 @@ autoCmd.command("update <id>").description("Edit an automation \u2014 rename, re
|
|
|
3191
3348
|
fail(new Error("pass either --script-js or --script-file, not both"));
|
|
3192
3349
|
return;
|
|
3193
3350
|
}
|
|
3194
|
-
const scriptJs = opts.scriptFile ?
|
|
3351
|
+
const scriptJs = opts.scriptFile ? readFileSync3(opts.scriptFile, "utf8") : opts.scriptJs;
|
|
3195
3352
|
const body = {
|
|
3196
3353
|
name: opts.name,
|
|
3197
3354
|
description: opts.description,
|
|
@@ -3269,4 +3426,8 @@ kvCmd.command("delete <slug> <key>").description("Delete a KV item").action(asyn
|
|
|
3269
3426
|
fail(e);
|
|
3270
3427
|
}
|
|
3271
3428
|
});
|
|
3429
|
+
{
|
|
3430
|
+
const first = process.argv[2];
|
|
3431
|
+
if (first !== "update" && first !== REFRESH_COMMAND) notifyIfOutdated(pkg.version);
|
|
3432
|
+
}
|
|
3272
3433
|
program.parseAsync(process.argv).catch(fail);
|