@erdoai/cli 0.118.0 → 0.119.1

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 +1 -1
  2. package/dist/index.js +106 -6
  3. package/package.json +8 -8
package/README.md CHANGED
@@ -11,7 +11,7 @@ npx @erdoai/cli --help # or run without installing
11
11
  erdo update # self-update to the latest published version
12
12
  ```
13
13
 
14
- Requires Node.js 18+. User-facing install docs: https://docs.erdo.ai/cli
14
+ Requires Node.js 22.13+. User-facing install docs: https://docs.erdo.ai/cli
15
15
 
16
16
  When a newer version is published, every command prints one line on **stderr**
17
17
  naming the installed version and `erdo update`. It is on stderr so stdout stays
package/dist/index.js CHANGED
@@ -135,7 +135,7 @@ function formatOrg(name, slug) {
135
135
  if (name && slug) return `${name} (${slug})`;
136
136
  return name || slug || "";
137
137
  }
138
- var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "error"]);
138
+ var TERMINAL_RUN_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled", "declined", "error"]);
139
139
  var POLL_STOP_STATUSES = /* @__PURE__ */ new Set([...TERMINAL_RUN_STATUSES, "awaiting_approval"]);
140
140
  var ErdoApiError = class extends Error {
141
141
  constructor(status, message) {
@@ -162,13 +162,13 @@ function networkErrorFor(method, path, err) {
162
162
  return new ErdoNetworkError(`${method} ${path} failed: ${detail}`, cause);
163
163
  }
164
164
  var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
165
- async function rawRequest(baseURL, token, orgId, projectId, method, path, body) {
165
+ async function rawRequest(baseURL, token, orgId, projectId, method, path, body, retryNetworkFailures = true) {
166
166
  const headers = { Authorization: `Bearer ${token}` };
167
167
  if (orgId) headers["X-Organization-ID"] = orgId;
168
168
  if (projectId) headers["X-Project-ID"] = projectId;
169
169
  if (body !== void 0) headers["Content-Type"] = "application/json";
170
170
  const upper = method.toUpperCase();
171
- const maxRetries = upper === "GET" || upper === "HEAD" ? 2 : 1;
171
+ const maxRetries = retryNetworkFailures ? upper === "GET" || upper === "HEAD" ? 2 : 1 : 0;
172
172
  let attempt = 0;
173
173
  while (true) {
174
174
  let res;
@@ -185,6 +185,9 @@ async function rawRequest(baseURL, token, orgId, projectId, method, path, body)
185
185
  attempt++;
186
186
  continue;
187
187
  }
188
+ if (!retryNetworkFailures) {
189
+ throw new ErdoNetworkError(`${netErr.message}. The launch may have started; inspect agent runs before retrying.`, netErr);
190
+ }
188
191
  throw netErr;
189
192
  }
190
193
  const text = await res.text();
@@ -260,7 +263,7 @@ var ErdoClient = class {
260
263
  process.stderr.write("org: token default (no org resolved \u2014 pass --org to pin)\n");
261
264
  }
262
265
  }
263
- request(method, path, body) {
266
+ request(method, path, body, retryNetworkFailures = true) {
264
267
  this.announceOrgOnce(method);
265
268
  return rawRequest(
266
269
  this.baseURL,
@@ -269,7 +272,8 @@ var ErdoClient = class {
269
272
  this.projectId,
270
273
  method,
271
274
  path,
272
- body
275
+ body,
276
+ retryNetworkFailures
273
277
  );
274
278
  }
275
279
  me() {
@@ -1058,7 +1062,7 @@ var ErdoClient = class {
1058
1062
  }
1059
1063
  // --- agents ---
1060
1064
  ask(input) {
1061
- return this.request("POST", "/v1/ask", input);
1065
+ return this.request("POST", "/v1/ask", input, !input.saved_ask_id);
1062
1066
  }
1063
1067
  createThread(input) {
1064
1068
  return this.request(
@@ -1075,6 +1079,25 @@ var ErdoClient = class {
1075
1079
  throw err;
1076
1080
  }
1077
1081
  }
1082
+ // The thread owns the current invocation identity. Approval resumes create a
1083
+ // new invocation while deliberately reusing the original agent run, so the
1084
+ // run list is not an authoritative source for cancellation.
1085
+ getThreadCompletionStatus(threadID) {
1086
+ return this.request(
1087
+ "GET",
1088
+ `/v1/threads/${encodeURIComponent(threadID)}/completion-status`
1089
+ );
1090
+ }
1091
+ // cancelThreadInvocation names the invocation being stopped. The thread
1092
+ // service refuses (HTTP 400, "the run already changed") when the thread has
1093
+ // moved on to another invocation, so a replacement run is never cancelled.
1094
+ cancelThreadInvocation(threadID, invocationID) {
1095
+ return this.request(
1096
+ "POST",
1097
+ `/v1/threads/${encodeURIComponent(threadID)}/cancel-invocation`,
1098
+ { invocation_id: invocationID }
1099
+ );
1100
+ }
1078
1101
  sendMessage(threadID, input) {
1079
1102
  return this.request(
1080
1103
  "POST",
@@ -3991,6 +4014,24 @@ agentCmd.command("send <threadId> <message>").description("Send a message to a t
3991
4014
  fail(e);
3992
4015
  }
3993
4016
  });
4017
+ agentCmd.command("cancel <threadId>").description("Request cancellation of a thread's active invocation; read the run to confirm it has stopped").option("--invocation <id>", "invocation to cancel (default: the thread's current invocation)").action(async (threadId, opts) => {
4018
+ try {
4019
+ const client = new ErdoClient();
4020
+ let invocationID = opts.invocation;
4021
+ if (!invocationID) {
4022
+ const current = await client.getThreadCompletionStatus(threadId);
4023
+ invocationID = current.invocation_id;
4024
+ if (!invocationID) throw new Error(`thread ${threadId} has no invocation to cancel`);
4025
+ }
4026
+ const result = await client.cancelThreadInvocation(threadId, invocationID);
4027
+ if (!result.success) {
4028
+ throw new Error("cancellation could not be confirmed; inspect the thread's latest run and retry");
4029
+ }
4030
+ console.log(JSON.stringify(result, null, 2));
4031
+ } catch (e) {
4032
+ fail(e);
4033
+ }
4034
+ });
3994
4035
  agentCmd.command("wait <threadId>").description("Re-attach to a thread's latest run and wait for it to finish; prints the answer").action(async (threadId) => {
3995
4036
  try {
3996
4037
  const client = new ErdoClient();
@@ -4007,6 +4048,65 @@ agentCmd.command("wait <threadId>").description("Re-attach to a thread's latest
4007
4048
  fail(e);
4008
4049
  }
4009
4050
  });
4051
+ var shortcutCmd = program.command("shortcut").description("Inspect and run saved Shortcuts with their stored agent and context");
4052
+ shortcutCmd.command("get <id>").description("Read the complete saved Ask, including its config and linked Knowledge").action(async (id) => {
4053
+ try {
4054
+ const response = await new ErdoClient().getKnowledge(id);
4055
+ if (response.object.type !== "ask" || ["archived", "deleted"].includes(response.object.status)) {
4056
+ throw new Error("Shortcut not found or inactive");
4057
+ }
4058
+ print(response);
4059
+ } catch (e) {
4060
+ fail(e);
4061
+ }
4062
+ });
4063
+ shortcutCmd.command("run <id>").description("Run the current saved prompt, agent, datasets and linked skills").option("--parameters <json>", "flat invocation data as JSON or @path; does not override saved configuration").option("--sync", "hold one HTTP request until completion; long runs can exceed proxy limits").option("--no-wait", "print the startup acknowledgement without waiting for the terminal run").action(async (id, opts) => {
4064
+ try {
4065
+ if (opts.sync && opts.wait === false) throw new Error("--sync cannot be combined with --no-wait");
4066
+ let parameters;
4067
+ if (opts.parameters !== void 0) {
4068
+ const value = JSON.parse(readMaybeFile(opts.parameters) ?? "");
4069
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
4070
+ throw new Error("Invocation parameters must be a plain object");
4071
+ }
4072
+ for (const [key, field] of Object.entries(value)) {
4073
+ if (!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(key)) throw new Error(`Invalid invocation parameter key: ${key}`);
4074
+ if (field !== null && typeof field !== "string" && typeof field !== "boolean" && (typeof field !== "number" || !Number.isFinite(field))) {
4075
+ throw new Error(`Invalid invocation parameter value for "${key}"`);
4076
+ }
4077
+ }
4078
+ if (JSON.stringify(value).length > 4096) throw new Error("Invocation parameters exceed the 4096-character limit");
4079
+ parameters = value;
4080
+ }
4081
+ const client = new ErdoClient();
4082
+ const response = await client.ask({ saved_ask_id: id, parameters, async: !opts.sync });
4083
+ if (typeof response.thread_id !== "string" || !response.thread_id.trim() || !opts.sync && response.status !== "running") throw new Error("Saved Shortcut did not start");
4084
+ if (opts.wait === false) {
4085
+ print({ thread_id: response.thread_id, message_id: response.message_id, status: response.status });
4086
+ return;
4087
+ }
4088
+ if (opts.sync) {
4089
+ if (response.status !== "completed" && response.status !== "success") throw new Error(`Saved Shortcut did not complete (status: ${response.status}; thread: ${response.thread_id})`);
4090
+ console.log(response.answer || `(status: ${response.status})`);
4091
+ console.log(`
4092
+ thread: ${response.thread_id}`);
4093
+ return;
4094
+ }
4095
+ process.stderr.write(`thread: ${response.thread_id}
4096
+ running\u2026`);
4097
+ const run = await client.waitForThreadRun(response.thread_id, { onTick: () => process.stderr.write(".") });
4098
+ process.stderr.write("\n");
4099
+ if (!run) throw new Error(timedOutMessage(response.thread_id));
4100
+ if (run.status === "awaiting_approval") throw new Error(awaitingApprovalMessage(response.thread_id));
4101
+ if (run.error) throw new Error(run.error);
4102
+ if (run.status !== "completed") throw new Error(`Saved Shortcut did not complete (status: ${run.status})`);
4103
+ console.log(run.output || `(status: ${run.status})`);
4104
+ console.log(`
4105
+ thread: ${response.thread_id}`);
4106
+ } catch (e) {
4107
+ fail(e);
4108
+ }
4109
+ });
4010
4110
  agentCmd.command("thread").description("Create a thread; prints its id").option("-n, --name <name>", "thread name").option("-d, --datasets <csv>", "dataset UUIDs to attach").option("--hidden", "keep the thread out of the organization's thread list (for diagnostic or scripted threads)").action(async (opts) => {
4011
4111
  try {
4012
4112
  const res = await new ErdoClient().createThread({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erdoai/cli",
3
- "version": "0.118.0",
3
+ "version": "0.119.1",
4
4
  "description": "Erdo CLI — drive datasets, pages, and evals from the terminal or CI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "scripts/postinstall.mjs"
12
12
  ],
13
13
  "engines": {
14
- "node": ">=18"
14
+ "node": "^22.13.0 || >=23.5.0"
15
15
  },
16
16
  "publishConfig": {
17
17
  "access": "public",
@@ -41,14 +41,14 @@
41
41
  "prepublishOnly": "npm run build"
42
42
  },
43
43
  "dependencies": {
44
- "@inquirer/prompts": "^7.5.0",
45
- "commander": "^12.1.0"
44
+ "@inquirer/prompts": "^8.7.3",
45
+ "commander": "^15.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@types/node": "^22.0.0",
49
- "tsup": "^8.0.0",
50
- "tsx": "^4.0.0",
51
- "typescript": "^5.7.0"
48
+ "@types/node": "^26.6.4",
49
+ "tsup": "^8.5.1",
50
+ "tsx": "^4.23.15",
51
+ "typescript": "^7.0.2"
52
52
  },
53
53
  "overrides": {
54
54
  "esbuild": "^0.28.1"