@erdoai/cli 0.44.0 → 0.45.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 +96 -17
- 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 {
|
|
@@ -535,6 +538,7 @@ var ErdoClient = class {
|
|
|
535
538
|
if (params?.offset) q.set("offset", String(params.offset));
|
|
536
539
|
if (params?.categories) q.set("categories", params.categories);
|
|
537
540
|
if (params?.scope) q.set("scope", params.scope);
|
|
541
|
+
if (params?.workstream_slug) q.set("workstream_slug", params.workstream_slug);
|
|
538
542
|
const qs = q.toString();
|
|
539
543
|
return this.request(
|
|
540
544
|
"GET",
|
|
@@ -656,6 +660,10 @@ var ErdoClient = class {
|
|
|
656
660
|
}
|
|
657
661
|
// The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
|
|
658
662
|
// `query` made every `erdo datasets query` fail with "question is required".
|
|
663
|
+
//
|
|
664
|
+
// `rows` carries the values in `columns` order — the answer to the question —
|
|
665
|
+
// and `output` the same result rendered to read. `row_count` is how many rows
|
|
666
|
+
// the generated SQL matched and can exceed `rows.length`, which is capped.
|
|
659
667
|
queryDataset(slug, question, timezone) {
|
|
660
668
|
return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/query-nl`, {
|
|
661
669
|
question,
|
|
@@ -850,6 +858,7 @@ var ErdoClient = class {
|
|
|
850
858
|
const params = new URLSearchParams();
|
|
851
859
|
if (opts.status) params.set("status", opts.status);
|
|
852
860
|
if (opts.limit) params.set("limit", String(opts.limit));
|
|
861
|
+
if (opts.workstream_slug) params.set("workstream_slug", opts.workstream_slug);
|
|
853
862
|
const qs = params.toString();
|
|
854
863
|
return this.request(
|
|
855
864
|
"GET",
|
|
@@ -1090,9 +1099,14 @@ async function browserLogin() {
|
|
|
1090
1099
|
}
|
|
1091
1100
|
|
|
1092
1101
|
// src/update.ts
|
|
1093
|
-
import { spawnSync } from "child_process";
|
|
1102
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
1103
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1104
|
+
import { dirname as dirname2 } from "path";
|
|
1094
1105
|
var PKG = "@erdoai/cli";
|
|
1095
1106
|
var MANUAL_HINT = `npm install -g ${PKG}@latest`;
|
|
1107
|
+
var CACHE_PATH = configPath("update-check.json");
|
|
1108
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1109
|
+
var REFRESH_COMMAND = "__update-check";
|
|
1096
1110
|
function parseVersion(v) {
|
|
1097
1111
|
const [core, pre = ""] = v.split("-", 2);
|
|
1098
1112
|
const nums = core.split(".").map((s) => Number.parseInt(s, 10) || 0);
|
|
@@ -1111,19 +1125,65 @@ function isNewer(latest, current) {
|
|
|
1111
1125
|
if (a.pre && !b.pre) return false;
|
|
1112
1126
|
return a.pre > b.pre;
|
|
1113
1127
|
}
|
|
1114
|
-
async function
|
|
1115
|
-
let latest;
|
|
1128
|
+
async function fetchLatestVersion(timeoutMs) {
|
|
1116
1129
|
try {
|
|
1117
1130
|
const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
|
|
1118
|
-
signal: AbortSignal.timeout(
|
|
1131
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1119
1132
|
});
|
|
1120
|
-
if (res.ok)
|
|
1133
|
+
if (res.ok) return (await res.json()).version;
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
return void 0;
|
|
1137
|
+
}
|
|
1138
|
+
function readVersionCache() {
|
|
1139
|
+
try {
|
|
1140
|
+
if (!existsSync2(CACHE_PATH)) return {};
|
|
1141
|
+
const raw = JSON.parse(readFileSync2(CACHE_PATH, "utf8"));
|
|
1142
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
1121
1143
|
} catch {
|
|
1144
|
+
return {};
|
|
1122
1145
|
}
|
|
1146
|
+
}
|
|
1147
|
+
function writeVersionCache(cache) {
|
|
1148
|
+
try {
|
|
1149
|
+
mkdirSync2(dirname2(CACHE_PATH), { recursive: true });
|
|
1150
|
+
writeFileSync2(CACHE_PATH, `${JSON.stringify(cache)}
|
|
1151
|
+
`);
|
|
1152
|
+
} catch {
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
async function refreshVersionCache() {
|
|
1156
|
+
const latest = await fetchLatestVersion(1e4);
|
|
1157
|
+
if (latest) writeVersionCache({ latest, checkedAt: Date.now() });
|
|
1158
|
+
}
|
|
1159
|
+
function notifyIfOutdated(current) {
|
|
1160
|
+
if (process.env.ERDO_NO_UPDATE_NOTIFIER) return;
|
|
1161
|
+
const cache = readVersionCache();
|
|
1162
|
+
if (cache.latest && isNewer(cache.latest, current)) {
|
|
1163
|
+
console.error(
|
|
1164
|
+
`erdo v${current} is out of date (v${cache.latest} is published) \u2014 run \`erdo update\`.`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const age = Date.now() - (cache.checkedAt ?? 0);
|
|
1168
|
+
if (age < CHECK_INTERVAL_MS) return;
|
|
1169
|
+
writeVersionCache({ ...cache, checkedAt: Date.now() });
|
|
1170
|
+
try {
|
|
1171
|
+
const entry = process.argv[1];
|
|
1172
|
+
if (!entry) return;
|
|
1173
|
+
spawn2(process.execPath, [entry, REFRESH_COMMAND], {
|
|
1174
|
+
detached: true,
|
|
1175
|
+
stdio: "ignore"
|
|
1176
|
+
}).unref();
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function update(current) {
|
|
1181
|
+
const latest = await fetchLatestVersion(1e4);
|
|
1123
1182
|
if (!latest) {
|
|
1124
1183
|
console.error(`Could not reach the npm registry. Try manually: ${MANUAL_HINT}`);
|
|
1125
1184
|
process.exit(1);
|
|
1126
1185
|
}
|
|
1186
|
+
writeVersionCache({ latest, checkedAt: Date.now() });
|
|
1127
1187
|
if (!isNewer(latest, current)) {
|
|
1128
1188
|
console.log(`Already up to date (v${current}).`);
|
|
1129
1189
|
return;
|
|
@@ -1146,7 +1206,7 @@ async function update(current) {
|
|
|
1146
1206
|
}
|
|
1147
1207
|
|
|
1148
1208
|
// src/index.ts
|
|
1149
|
-
var pkg = JSON.parse(
|
|
1209
|
+
var pkg = JSON.parse(readFileSync3(new URL("../package.json", import.meta.url), "utf8"));
|
|
1150
1210
|
function fail(err) {
|
|
1151
1211
|
if (err instanceof ErdoApiError) {
|
|
1152
1212
|
console.error(`Error ${err.status}: ${err.message}`);
|
|
@@ -1307,6 +1367,9 @@ program.command("update").description("Update the CLI to the latest published ve
|
|
|
1307
1367
|
fail(e);
|
|
1308
1368
|
}
|
|
1309
1369
|
});
|
|
1370
|
+
program.command(REFRESH_COMMAND, { hidden: true }).description("Internal: refresh the remembered latest published version").action(async () => {
|
|
1371
|
+
await refreshVersionCache();
|
|
1372
|
+
});
|
|
1310
1373
|
var org = program.command("org").description("Manage the active organization");
|
|
1311
1374
|
org.command("list").description("List your organizations (* = active)").action(async () => {
|
|
1312
1375
|
try {
|
|
@@ -2309,9 +2372,13 @@ runsCmd.command("get <id>").description("Show an agent run (status, output, trac
|
|
|
2309
2372
|
}
|
|
2310
2373
|
});
|
|
2311
2374
|
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) => {
|
|
2375
|
+
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
2376
|
try {
|
|
2314
|
-
const res = await new ErdoClient().listApprovals(
|
|
2377
|
+
const res = await new ErdoClient().listApprovals({
|
|
2378
|
+
status: opts.status,
|
|
2379
|
+
limit: opts.limit,
|
|
2380
|
+
workstream_slug: opts.workstream
|
|
2381
|
+
});
|
|
2315
2382
|
if (opts.json) {
|
|
2316
2383
|
print(res);
|
|
2317
2384
|
return;
|
|
@@ -2328,7 +2395,7 @@ approvalsCmd.command("list").description("List approval requests, optionally fil
|
|
|
2328
2395
|
});
|
|
2329
2396
|
approvalsCmd.command("decide <id>").description("Approve or reject a pending approval request").option("--approve", "approve the request").option("--reject", "reject the request").option(
|
|
2330
2397
|
"--scope <scope>",
|
|
2331
|
-
"once (default) | always_this_job | always_org | always_user",
|
|
2398
|
+
"once (default) | always_this_job | always_this_workstream | always_org | always_user",
|
|
2332
2399
|
"once"
|
|
2333
2400
|
).action(async (id, opts) => {
|
|
2334
2401
|
try {
|
|
@@ -2466,14 +2533,15 @@ program.command("activity").alias("feed").description(
|
|
|
2466
2533
|
).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
2534
|
"--categories <list>",
|
|
2468
2535
|
"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(
|
|
2536
|
+
).option("--scope <scope>", "following | all (default all)").option("--workstream <slug>", "only this workstream").option("--json", "raw JSON response").action(
|
|
2470
2537
|
async (opts) => {
|
|
2471
2538
|
try {
|
|
2472
2539
|
const feed = await new ErdoClient().listActivityFeed({
|
|
2473
2540
|
limit: opts.limit,
|
|
2474
2541
|
offset: opts.offset,
|
|
2475
2542
|
categories: opts.categories,
|
|
2476
|
-
scope: opts.scope
|
|
2543
|
+
scope: opts.scope,
|
|
2544
|
+
workstream_slug: opts.workstream
|
|
2477
2545
|
});
|
|
2478
2546
|
if (opts.json) {
|
|
2479
2547
|
print(feed);
|
|
@@ -2676,7 +2744,7 @@ reviewsCmd.command("decide <id>").description(
|
|
|
2676
2744
|
var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
|
|
2677
2745
|
function readMaybeFile(v) {
|
|
2678
2746
|
if (!v) return void 0;
|
|
2679
|
-
return v.startsWith("@") ?
|
|
2747
|
+
return v.startsWith("@") ? readFileSync3(v.slice(1), "utf8") : v;
|
|
2680
2748
|
}
|
|
2681
2749
|
function grantList(v) {
|
|
2682
2750
|
if (v === void 0) return void 0;
|
|
@@ -2855,14 +2923,21 @@ datasetsCmd.command("list").description("List datasets").option(
|
|
|
2855
2923
|
fail(e);
|
|
2856
2924
|
}
|
|
2857
2925
|
});
|
|
2858
|
-
datasetsCmd.command("query <slug> <question>").description(
|
|
2926
|
+
datasetsCmd.command("query <slug> <question>").description(
|
|
2927
|
+
"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`."
|
|
2928
|
+
).action(async (slug, question) => {
|
|
2859
2929
|
try {
|
|
2860
2930
|
print(await new ErdoClient().queryDataset(slug, question));
|
|
2861
2931
|
} catch (e) {
|
|
2862
2932
|
fail(e);
|
|
2863
2933
|
}
|
|
2864
2934
|
});
|
|
2865
|
-
datasetsCmd.command("fetch <slug>").description(
|
|
2935
|
+
datasetsCmd.command("fetch <slug>").description(
|
|
2936
|
+
"Read rows from a dataset \u2014 the deterministic read, and the one to use for anything mechanical or scripted. Returns {columns, rows, row_count}."
|
|
2937
|
+
).option(
|
|
2938
|
+
"-q, --sql <query>",
|
|
2939
|
+
"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."
|
|
2940
|
+
).option("-l, --limit <n>", "max rows to return", (v) => parseInt(v, 10)).option(
|
|
2866
2941
|
"-f, --filter <name>",
|
|
2867
2942
|
"opt into a saved filter by name (repeatable); additive on top of the dataset's default filters (see: erdo datasets filter list <slug>)",
|
|
2868
2943
|
(v, acc) => acc.concat(v),
|
|
@@ -2883,7 +2958,7 @@ datasetsCmd.command("fetch <slug>").description("Fetch rows from a dataset, opti
|
|
|
2883
2958
|
var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
|
|
2884
2959
|
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
2960
|
try {
|
|
2886
|
-
const content =
|
|
2961
|
+
const content = readFileSync3(file);
|
|
2887
2962
|
if (content.byteLength > MAX_UPLOAD_BYTES) {
|
|
2888
2963
|
fail(
|
|
2889
2964
|
new Error(
|
|
@@ -3191,7 +3266,7 @@ autoCmd.command("update <id>").description("Edit an automation \u2014 rename, re
|
|
|
3191
3266
|
fail(new Error("pass either --script-js or --script-file, not both"));
|
|
3192
3267
|
return;
|
|
3193
3268
|
}
|
|
3194
|
-
const scriptJs = opts.scriptFile ?
|
|
3269
|
+
const scriptJs = opts.scriptFile ? readFileSync3(opts.scriptFile, "utf8") : opts.scriptJs;
|
|
3195
3270
|
const body = {
|
|
3196
3271
|
name: opts.name,
|
|
3197
3272
|
description: opts.description,
|
|
@@ -3269,4 +3344,8 @@ kvCmd.command("delete <slug> <key>").description("Delete a KV item").action(asyn
|
|
|
3269
3344
|
fail(e);
|
|
3270
3345
|
}
|
|
3271
3346
|
});
|
|
3347
|
+
{
|
|
3348
|
+
const first = process.argv[2];
|
|
3349
|
+
if (first !== "update" && first !== REFRESH_COMMAND) notifyIfOutdated(pkg.version);
|
|
3350
|
+
}
|
|
3272
3351
|
program.parseAsync(process.argv).catch(fail);
|