@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.
Files changed (3) hide show
  1. package/README.md +58 -11
  2. package/dist/index.js +191 -30
  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 {
@@ -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 update(current) {
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(1e4)
1138
+ signal: AbortSignal.timeout(timeoutMs)
1119
1139
  });
1120
- if (res.ok) latest = (await res.json()).version;
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(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
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(opts);
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
- ).action(async (id, opts) => {
2334
- try {
2335
- if (opts.approve === opts.reject) {
2336
- throw new Error("specify exactly one of --approve or --reject");
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("@") ? readFileSync2(v.slice(1), "utf8") : v;
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("Ask a natural-language question of a dataset").action(async (slug, question) => {
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("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(
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 = readFileSync2(file);
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("Query page analytics (how published pages perform with real visitors)");
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 ? readFileSync2(opts.scriptFile, "utf8") : opts.scriptJs;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {