@erdoai/cli 0.43.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 +133 -20
  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,
@@ -788,14 +796,18 @@ var ErdoClient = class {
788
796
  }
789
797
  // --- automations (heartbeats) ---
790
798
  listHeartbeats() {
791
- return this.request(
792
- "GET",
793
- "/v1/heartbeats"
794
- );
799
+ return this.request("GET", "/v1/heartbeats");
795
800
  }
796
801
  runHeartbeat(id) {
797
802
  return this.request("POST", `/v1/heartbeats/${encodeURIComponent(id)}/run`, {});
798
803
  }
804
+ updateHeartbeat(id, body) {
805
+ return this.request(
806
+ "PATCH",
807
+ `/v1/heartbeats/${encodeURIComponent(id)}`,
808
+ body
809
+ );
810
+ }
799
811
  setHeartbeatState(id, state) {
800
812
  return this.request(
801
813
  "POST",
@@ -846,6 +858,7 @@ var ErdoClient = class {
846
858
  const params = new URLSearchParams();
847
859
  if (opts.status) params.set("status", opts.status);
848
860
  if (opts.limit) params.set("limit", String(opts.limit));
861
+ if (opts.workstream_slug) params.set("workstream_slug", opts.workstream_slug);
849
862
  const qs = params.toString();
850
863
  return this.request(
851
864
  "GET",
@@ -1086,9 +1099,14 @@ async function browserLogin() {
1086
1099
  }
1087
1100
 
1088
1101
  // src/update.ts
1089
- 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";
1090
1105
  var PKG = "@erdoai/cli";
1091
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";
1092
1110
  function parseVersion(v) {
1093
1111
  const [core, pre = ""] = v.split("-", 2);
1094
1112
  const nums = core.split(".").map((s) => Number.parseInt(s, 10) || 0);
@@ -1107,19 +1125,65 @@ function isNewer(latest, current) {
1107
1125
  if (a.pre && !b.pre) return false;
1108
1126
  return a.pre > b.pre;
1109
1127
  }
1110
- async function update(current) {
1111
- let latest;
1128
+ async function fetchLatestVersion(timeoutMs) {
1112
1129
  try {
1113
1130
  const res = await fetch(`https://registry.npmjs.org/${PKG}/latest`, {
1114
- signal: AbortSignal.timeout(1e4)
1131
+ signal: AbortSignal.timeout(timeoutMs)
1115
1132
  });
1116
- 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 : {};
1143
+ } catch {
1144
+ return {};
1145
+ }
1146
+ }
1147
+ function writeVersionCache(cache) {
1148
+ try {
1149
+ mkdirSync2(dirname2(CACHE_PATH), { recursive: true });
1150
+ writeFileSync2(CACHE_PATH, `${JSON.stringify(cache)}
1151
+ `);
1117
1152
  } catch {
1118
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);
1119
1182
  if (!latest) {
1120
1183
  console.error(`Could not reach the npm registry. Try manually: ${MANUAL_HINT}`);
1121
1184
  process.exit(1);
1122
1185
  }
1186
+ writeVersionCache({ latest, checkedAt: Date.now() });
1123
1187
  if (!isNewer(latest, current)) {
1124
1188
  console.log(`Already up to date (v${current}).`);
1125
1189
  return;
@@ -1142,7 +1206,7 @@ async function update(current) {
1142
1206
  }
1143
1207
 
1144
1208
  // src/index.ts
1145
- 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"));
1146
1210
  function fail(err) {
1147
1211
  if (err instanceof ErdoApiError) {
1148
1212
  console.error(`Error ${err.status}: ${err.message}`);
@@ -1303,6 +1367,9 @@ program.command("update").description("Update the CLI to the latest published ve
1303
1367
  fail(e);
1304
1368
  }
1305
1369
  });
1370
+ program.command(REFRESH_COMMAND, { hidden: true }).description("Internal: refresh the remembered latest published version").action(async () => {
1371
+ await refreshVersionCache();
1372
+ });
1306
1373
  var org = program.command("org").description("Manage the active organization");
1307
1374
  org.command("list").description("List your organizations (* = active)").action(async () => {
1308
1375
  try {
@@ -2305,9 +2372,13 @@ runsCmd.command("get <id>").description("Show an agent run (status, output, trac
2305
2372
  }
2306
2373
  });
2307
2374
  var approvalsCmd = program.command("approvals").description("List and decide approval requests (actions agents paused on)");
2308
- 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) => {
2309
2376
  try {
2310
- 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
+ });
2311
2382
  if (opts.json) {
2312
2383
  print(res);
2313
2384
  return;
@@ -2324,7 +2395,7 @@ approvalsCmd.command("list").description("List approval requests, optionally fil
2324
2395
  });
2325
2396
  approvalsCmd.command("decide <id>").description("Approve or reject a pending approval request").option("--approve", "approve the request").option("--reject", "reject the request").option(
2326
2397
  "--scope <scope>",
2327
- "once (default) | always_this_job | always_org | always_user",
2398
+ "once (default) | always_this_job | always_this_workstream | always_org | always_user",
2328
2399
  "once"
2329
2400
  ).action(async (id, opts) => {
2330
2401
  try {
@@ -2462,14 +2533,15 @@ program.command("activity").alias("feed").description(
2462
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(
2463
2534
  "--categories <list>",
2464
2535
  "comma-separated: attention,approval,workstream,catalog,job,heartbeat (default: attention,approval,workstream)"
2465
- ).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(
2466
2537
  async (opts) => {
2467
2538
  try {
2468
2539
  const feed = await new ErdoClient().listActivityFeed({
2469
2540
  limit: opts.limit,
2470
2541
  offset: opts.offset,
2471
2542
  categories: opts.categories,
2472
- scope: opts.scope
2543
+ scope: opts.scope,
2544
+ workstream_slug: opts.workstream
2473
2545
  });
2474
2546
  if (opts.json) {
2475
2547
  print(feed);
@@ -2672,7 +2744,7 @@ reviewsCmd.command("decide <id>").description(
2672
2744
  var pagesCmd = program.command("pages").description("Create and manage pages/artifacts");
2673
2745
  function readMaybeFile(v) {
2674
2746
  if (!v) return void 0;
2675
- return v.startsWith("@") ? readFileSync2(v.slice(1), "utf8") : v;
2747
+ return v.startsWith("@") ? readFileSync3(v.slice(1), "utf8") : v;
2676
2748
  }
2677
2749
  function grantList(v) {
2678
2750
  if (v === void 0) return void 0;
@@ -2851,14 +2923,21 @@ datasetsCmd.command("list").description("List datasets").option(
2851
2923
  fail(e);
2852
2924
  }
2853
2925
  });
2854
- 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) => {
2855
2929
  try {
2856
2930
  print(await new ErdoClient().queryDataset(slug, question));
2857
2931
  } catch (e) {
2858
2932
  fail(e);
2859
2933
  }
2860
2934
  });
2861
- 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(
2862
2941
  "-f, --filter <name>",
2863
2942
  "opt into a saved filter by name (repeatable); additive on top of the dataset's default filters (see: erdo datasets filter list <slug>)",
2864
2943
  (v, acc) => acc.concat(v),
@@ -2879,7 +2958,7 @@ datasetsCmd.command("fetch <slug>").description("Fetch rows from a dataset, opti
2879
2958
  var MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
2880
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) => {
2881
2960
  try {
2882
- const content = readFileSync2(file);
2961
+ const content = readFileSync3(file);
2883
2962
  if (content.byteLength > MAX_UPLOAD_BYTES) {
2884
2963
  fail(
2885
2964
  new Error(
@@ -3177,6 +3256,36 @@ autoCmd.command("run <id>").description("Trigger an automation now").action(asyn
3177
3256
  fail(e);
3178
3257
  }
3179
3258
  });
3259
+ autoCmd.command("update <id>").description("Edit an automation \u2014 rename, reschedule, or replace its script/instructions").option("--name <name>", "New name").option("--description <text>", "New description").option("--instructions <text>", "Replacement instructions (agent automation)").option("--script-js <js>", "Replacement script body (scripted automation)").option(
3260
+ "--script-file <path>",
3261
+ "Read the replacement script body from a file (scripted automation)"
3262
+ ).option("--interval <minutes>", "New interval in minutes (minimum 5)", (v) => parseInt(v, 10)).option("--timezone <tz>", "New scheduling timezone, e.g. America/New_York").action(
3263
+ async (id, opts) => {
3264
+ try {
3265
+ if (opts.scriptJs && opts.scriptFile) {
3266
+ fail(new Error("pass either --script-js or --script-file, not both"));
3267
+ return;
3268
+ }
3269
+ const scriptJs = opts.scriptFile ? readFileSync3(opts.scriptFile, "utf8") : opts.scriptJs;
3270
+ const body = {
3271
+ name: opts.name,
3272
+ description: opts.description,
3273
+ instructions: opts.instructions,
3274
+ script_js: scriptJs,
3275
+ interval_minutes: opts.interval,
3276
+ timezone: opts.timezone
3277
+ };
3278
+ if (Object.values(body).every((v) => v === void 0)) {
3279
+ fail(new Error("nothing to update \u2014 pass at least one field to change"));
3280
+ return;
3281
+ }
3282
+ const h = await new ErdoClient().updateHeartbeat(id, body);
3283
+ console.log(`${h.id} ${h.state} ${h.name}`);
3284
+ } catch (e) {
3285
+ fail(e);
3286
+ }
3287
+ }
3288
+ );
3180
3289
  autoCmd.command("disable <id>").description("Disable an automation so it stops running").action(async (id) => {
3181
3290
  try {
3182
3291
  const h = await new ErdoClient().setHeartbeatState(id, "disabled");
@@ -3235,4 +3344,8 @@ kvCmd.command("delete <slug> <key>").description("Delete a KV item").action(asyn
3235
3344
  fail(e);
3236
3345
  }
3237
3346
  });
3347
+ {
3348
+ const first = process.argv[2];
3349
+ if (first !== "update" && first !== REFRESH_COMMAND) notifyIfOutdated(pkg.version);
3350
+ }
3238
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.43.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": {