@erdoai/cli 0.120.1 → 0.122.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 (2) hide show
  1. package/dist/index.js +67 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -186,7 +186,7 @@ async function rawRequest(baseURL, token, orgId, projectId, method, path, body,
186
186
  continue;
187
187
  }
188
188
  if (!retryNetworkFailures) {
189
- throw new ErdoNetworkError(`${netErr.message}. The launch may have started; inspect agent runs before retrying.`, netErr);
189
+ throw new ErdoNetworkError(`${netErr.message}. The request may have succeeded; inspect the affected resource before retrying.`, netErr);
190
190
  }
191
191
  throw netErr;
192
192
  }
@@ -1200,6 +1200,12 @@ var ErdoClient = class {
1200
1200
  updateDatasetSchema(slug, input) {
1201
1201
  return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/schema`, input);
1202
1202
  }
1203
+ // Dataset row writes are deliberately never replayed after a network
1204
+ // failure: the server may have committed the rows before the response was
1205
+ // lost, and repeating an append or upsert can change the result.
1206
+ writeDatasetRows(slug, input) {
1207
+ return this.request("POST", `/v1/datasets/${encodeURIComponent(slug)}/rows`, input, false);
1208
+ }
1203
1209
  // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending // The endpoint's field is `question` (QueryDataNaturalLanguageInput). Sending
1204
1210
  // `query` made every `erdo datasets query` fail with "question is required".
1205
1211
  //
@@ -3012,14 +3018,15 @@ ${res.from} to ${res.to} (${res.timezone} days)
3012
3018
  }
3013
3019
  );
3014
3020
  var tokenCmd = program.command("token").description("Manage your API tokens (account-level credentials)");
3015
- tokenCmd.command("create").description("Mint a new API token; the secret is printed ONCE").requiredOption("--name <name>", "a label for the token").option("--expires-days <n>", "days until expiry (default: 30)", (v) => parseInt(v, 10)).addOption(
3021
+ tokenCmd.command("create").description("Mint a new API token; the secret is printed ONCE").requiredOption("--name <name>", "a label for the token").option("--expires-days <n>", "days until expiry (default: 30)", (v) => parseInt(v, 10)).option("--read-only", "mint a read-only token: it can read through the API in any org you belong to, and never write").addOption(
3016
3022
  orgOption("the token's default org (defaults to your active org; must be one you belong to)")
3017
3023
  ).action(async (opts) => {
3018
3024
  try {
3019
3025
  const res = await new ErdoClient().createToken({
3020
3026
  name: opts.name,
3021
3027
  expires_in_days: opts.expiresDays ?? 30,
3022
- organization_id: opts.org
3028
+ organization_id: opts.org,
3029
+ ...opts.readOnly ? { access: "read" } : {}
3023
3030
  });
3024
3031
  process.stderr.write(
3025
3032
  "Store this token now \u2014 it is shown only once and cannot be retrieved again.\n"
@@ -3031,8 +3038,8 @@ id: ${res.id}
3031
3038
  hint: ${res.hint}
3032
3039
  expires: ${res.expires_at}
3033
3040
  default org: ${res.default_org ? `${res.default_org.name} (${res.default_org.id})` : "(none)"}
3034
- This token acts as you in any org you belong to; pass --org on a command to target another.
3035
- `
3041
+ access: ${res.access ?? "full"}
3042
+ ` + (res.access === "read" ? "This token can read as you in any org you belong to and cannot change anything; pass --org on a command to target another.\n" : "This token acts as you in any org you belong to; pass --org on a command to target another.\n")
3036
3043
  );
3037
3044
  } catch (e) {
3038
3045
  fail(e);
@@ -3048,7 +3055,7 @@ tokenCmd.command("list").description("List your API tokens (never shows the secr
3048
3055
  for (const t of tokens) {
3049
3056
  const org2 = t.default_org ? t.default_org.name || t.default_org.id : "-";
3050
3057
  const used = t.last_used_at ?? "never";
3051
- console.log(`${t.id} ${t.name || "-"} ${t.hint} default-org:${org2} expires:${t.expires_at} used:${used}`);
3058
+ console.log(`${t.id} ${t.name || "-"} ${t.hint} access:${t.access ?? "full"} default-org:${org2} expires:${t.expires_at} used:${used}`);
3052
3059
  }
3053
3060
  } catch (e) {
3054
3061
  fail(e);
@@ -4191,13 +4198,13 @@ runsCmd.command("list").description("List agent runs (filter by agent OR thread
4191
4198
  fail(e);
4192
4199
  }
4193
4200
  });
4194
- runsCmd.command("get <id>").description("Show an agent run (status, output, trace metadata)").option("--resources", "also show the skills, knowledge and entities the run used").option("--steps", "also show the run's iterations, tool calls and sub-agent invocations").action(async (id, opts) => {
4201
+ runsCmd.command("get <id>").description("Show an agent run (status, output, trace metadata)").option("--resources", "also show the skills, knowledge and entities the run used").option("--steps", "also show the run's iterations, tool calls and sub-agent invocations").option("--json", "print the full JSON response, including requested resources and steps").action(async (id, opts) => {
4195
4202
  try {
4196
4203
  const include = [];
4197
4204
  if (opts.resources) include.push("resources");
4198
4205
  if (opts.steps) include.push("steps");
4199
4206
  const detail = await new ErdoClient().getAgentRun(id, include);
4200
- if (include.length === 0) {
4207
+ if (opts.json || include.length === 0) {
4201
4208
  print(detail);
4202
4209
  return;
4203
4210
  }
@@ -6657,6 +6664,53 @@ datasetsCmd.command("purposes").description(
6657
6664
  fail(e);
6658
6665
  }
6659
6666
  });
6667
+ datasetsCmd.command("write <slug>").description(
6668
+ "Write rows to a dataset. Without --key-column rows append. File and writable PostgreSQL datasets support keyed replace or merge. ClickHouse-backed writes always append, even with a key, and refuse merge."
6669
+ ).requiredOption("--rows <json-or-@file>", "JSON array of row objects, or @path to a JSON file").option("--key-column <columns>", "non-blank key column, or comma-separated columns for a composite key").option("--write-mode <replace|merge>", "replace matching fields (files clear omitted fields; PostgreSQL updates batch columns), or merge; either explicit mode requires --key-column").option("--overwrite-columns <csv>", "comma-separated columns merge mode may overwrite").action(
6670
+ async (slug, opts) => {
6671
+ try {
6672
+ const rawRows = readMaybeFile(opts.rows);
6673
+ let parsed;
6674
+ try {
6675
+ parsed = JSON.parse(rawRows ?? "");
6676
+ } catch (error) {
6677
+ throw new Error(`--rows must contain valid JSON: ${error instanceof Error ? error.message : String(error)}`);
6678
+ }
6679
+ if (!Array.isArray(parsed) || parsed.some((row) => row === null || typeof row !== "object" || Array.isArray(row))) {
6680
+ throw new Error("--rows must contain a JSON array of non-null objects");
6681
+ }
6682
+ const writeMode = opts.writeMode;
6683
+ if (writeMode !== void 0 && writeMode !== "replace" && writeMode !== "merge") {
6684
+ throw new Error('--write-mode must be "replace" or "merge"');
6685
+ }
6686
+ if (writeMode !== void 0 && !opts.keyColumn?.trim()) {
6687
+ throw new Error(`--write-mode "${writeMode}" requires a non-blank --key-column`);
6688
+ }
6689
+ if (opts.keyColumn !== void 0 && !opts.keyColumn.split(",").some((column) => column.trim())) {
6690
+ throw new Error("--key-column must name at least one non-blank column");
6691
+ }
6692
+ let overwriteColumns;
6693
+ if (opts.overwriteColumns !== void 0) {
6694
+ if (writeMode !== "merge") {
6695
+ throw new Error("--overwrite-columns requires --write-mode merge");
6696
+ }
6697
+ overwriteColumns = opts.overwriteColumns.split(",").map((column) => column.trim());
6698
+ if (overwriteColumns.some((column) => !column)) {
6699
+ throw new Error("--overwrite-columns must not contain empty column names");
6700
+ }
6701
+ }
6702
+ const result = await new ErdoClient().writeDatasetRows(slug, {
6703
+ rows: parsed,
6704
+ ...opts.keyColumn !== void 0 ? { key_column: opts.keyColumn } : {},
6705
+ ...writeMode !== void 0 ? { write_mode: writeMode } : {},
6706
+ ...overwriteColumns !== void 0 ? { overwrite_columns: overwriteColumns } : {}
6707
+ });
6708
+ print(result);
6709
+ } catch (e) {
6710
+ fail(e);
6711
+ }
6712
+ }
6713
+ );
6660
6714
  function readSchemaFile(path) {
6661
6715
  const parsed = JSON.parse(readFileSync4(path, "utf8"));
6662
6716
  const schema = Array.isArray(parsed) ? { columns: parsed } : parsed;
@@ -7095,7 +7149,7 @@ refreshCmd.command("set <slug>").description(
7095
7149
  ).option(
7096
7150
  "--mode <replace|upsert>",
7097
7151
  "replace overwrites every row with what the recipe returned; upsert merges by --key-column and keeps rows outside the returned window. Omit it and you get upsert when you passed --key-column and replace when you did not, since a key column is only meaningful to a merge"
7098
- ).option("--key-column <c>", "the column an upsert merges on; required with --mode upsert").option(
7152
+ ).option("--key-column <c>", "non-blank column, or comma-separated columns, an upsert merges on; required with --mode upsert").option(
7099
7153
  "--schedule <cron>",
7100
7154
  "a cron expression to run the refresh on a fixed cadence. Omit it and the refresh runs on view instead \u2014 cheaper, because it never runs while nobody is looking"
7101
7155
  ).option("--timezone <tz>", "IANA timezone the cron expression is read in (default UTC)").option(
@@ -7145,6 +7199,10 @@ refreshCmd.command("set <slug>").description(
7145
7199
  fail(new Error("--mode must be replace or upsert"));
7146
7200
  return;
7147
7201
  }
7202
+ if (opts.keyColumn !== void 0 && !opts.keyColumn.split(",").some((column) => column.trim())) {
7203
+ fail(new Error("--key-column must name at least one non-blank column"));
7204
+ return;
7205
+ }
7148
7206
  if (opts.mode === "upsert" && !opts.keyColumn) {
7149
7207
  fail(new Error("--mode upsert needs --key-column <c>: the column the merge keys on"));
7150
7208
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.120.1",
3
+ "version": "0.122.0",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {