@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.
Files changed (3) hide show
  1. package/README.md +58 -11
  2. package/dist/index.js +96 -17
  3. 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
- Publishing is automated by `.github/workflows/publish-cli.yml`: pushing a
90
- `cli-v*` tag builds, typechecks, stamps the version from the tag, and runs
91
- `npm publish` to the npm registry (auth via the repo's `NPM_TOKEN` secret).
92
-
93
- ```bash
94
- # bump cli/package.json "version", commit, then tag + push:
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
- The tag's version (`cli-v0.4.1` `0.4.1`) is the published version; keep
100
- `cli/package.json` in sync so `erdo --version` matches locally.
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 readFileSync2 } from "fs";
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 update(current) {
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(1e4)
1131
+ signal: AbortSignal.timeout(timeoutMs)
1119
1132
  });
1120
- if (res.ok) latest = (await res.json()).version;
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(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
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(opts);
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("@") ? readFileSync2(v.slice(1), "utf8") : v;
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("Ask a natural-language question of a dataset").action(async (slug, question) => {
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("Fetch rows from a dataset, optionally shaped by SQL and named filters").option("-q, --sql <query>", "SQL query to filter/transform the rows").option("-l, --limit <n>", "max rows to return", (v) => parseInt(v, 10)).option(
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 = readFileSync2(file);
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 ? readFileSync2(opts.scriptFile, "utf8") : opts.scriptJs;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {